├── .gitignore ├── README.md ├── fonts ├── Material-Design-Iconic-Font.ttf ├── Material-Design-Iconic-Round.ttf ├── osc.ttf ├── stats mono.ttf ├── stats.ttf └── subs.ttf ├── input.conf ├── mpv_linux.conf ├── mpv_osx.conf ├── mpv_windows.conf ├── script-opts ├── playlistmanager.conf ├── quality-menu.conf └── stats.conf ├── scripts ├── quality-menu.lua ├── Mac_Integration.lua ├── acompressor.lua ├── appendURL.lua ├── audio-osc.lua ├── autoload.lua ├── modernx.lua ├── playlistmanager.lua ├── quality-menu.lua ├── seek-to.lua └── webm.lua └── shaders ├── CfL_Prediction.glsl ├── FastBilateral.glsl ├── JointBilateral.glsl └── ravu-zoom-ar-r3-rgb.hook /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Shaders/.DS_Store 3 | scripts/.DS_Store 4 | script-opts/.DS_Store 5 | mpv.conf 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mpv-settings 2 | my settings for MPV (Windows/Mac/Linux Compatible) 3 | 4 | might not be suitable for your pc, but if you have any recommendations, just tell me, 5 | or just make a pull request. If i like it, i put it in. 6 | 7 | # Documentation 8 | Here goes all information about scripts and upscaler used. (WIP) 9 | 10 | # List of Scripts used in my mpv-settings: 11 | 12 | - Mac_Integration.lua - This script enables a few shortcuts which Mac users are familiar with. See scripts/Mac_Integration.lua for more infos. 13 | - acompressor.lua - a simple audio compression script which can normalize your audio of the files played with mpv. See scripts/acompressor.lua for more infos. 14 | - appendURL.lua - when mpv is opened, you can copy paste a URL in to play from. 15 | - audio-osc.lua - different on screen controls for audio-only playback. 16 | - autoload.lua - preloads all files in a folder into a playlist. 17 | - seek-to.lua - when "t" is pressed, you can seek to a specific part of the video/audio you are currently watching. 18 | - webm.lua - Simple WebM maker for mpv. By default, the script is activated by the W (shift+w) key. 19 | - playlistmanager.lua - This script allows you to see and interact with your playlist in an intuitive way. SHIFT+ENTER = playlist 20 | - modernx.lua - A modern OSC UI replacement for MPV that retains the functionality of the default OSC. 21 | - quality-menu.lua - Allows you to change the streamed video and audio quality (ytdl-format) on the fly. Simply open the video or audio menu, select your prefered format and confirm your choice. The keybindings for opening the menus are configured in input.conf, and everthing else is configured in quality-menu.conf. By default: List Video Formats: F (shift+f), List Audio Formats: Alt+f, Reload: Ctrl+r 22 | 23 | # Installation 24 | Depending on your Operating System, you need to place the stuff inside the zip in a certain directory. 25 | The root directory needs to look like this (Should be considered a Tree View example): 26 | 27 | 28 | >Roaming 29 | 30 | >>mpv 31 | 32 | >>>input.conf 33 | 34 | >>>mpv.conf 35 | 36 | >>>shaders 37 | 38 | >>>script-opts 39 | 40 | >>>scripts 41 | 42 | >>>fonts 43 | 44 | you need to rename the proper config you want to use to mpv.conf. 45 | 46 | example: mpv-windows.conf -> mpv.conf 47 | 48 | # WINDOWS INSTALLATION 49 | > "C:\Users\ %Username% \AppData\Roaming\mpv" 50 | 51 | # MAC INSTALLATION 52 | Path: 53 | 54 | /USERNAME/.config/mpv 55 | 56 | Tested Apple Devices on latest OS (BigSur at the time of writing the readme): 57 | 58 | - Base Macbook Pro 2018 13" 59 | - Macbook Air M1 8Cpu/8Gpu 60 | - Macbook Pro M2 8Cpu/10Gpu 61 | 62 | 63 | # LINUX INSTALLATION 64 | Path: 65 | /home/user/.config/mpv 66 | 67 | /user/ is always the name of the user who wants to use mpv. 68 | 69 | # Community and Discord Help Server 70 | 71 | I also have a Server for mpv-settings and AIO_Video_Enhancer. You can join here: https://discord.gg/WjtkbcQ (currently locked, but i also accept DMs) 72 | 73 | Discord: @Tsubajashi 74 | 75 | # DONATIONS 76 | if you like to donate, heres a link: [Click Here to get to the Donation Page (Paypal)](https://www.paypal.com/donate/?hosted_button_id=24L5ZAN3AXZ8N) 77 | if you are in a bad spot financially, please use it to get yourself nice things. :) 78 | i also released a patreon if you want to support me on a monthly basis, its a "pay what you want" patreon as i dont have scheduled releases. https://www.patreon.com/tsubajashi 79 | -------------------------------------------------------------------------------- /fonts/Material-Design-Iconic-Font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tsubajashi/mpv-settings/f859a9a3b722ce4b27977934a3798c0a99549852/fonts/Material-Design-Iconic-Font.ttf -------------------------------------------------------------------------------- /fonts/Material-Design-Iconic-Round.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tsubajashi/mpv-settings/f859a9a3b722ce4b27977934a3798c0a99549852/fonts/Material-Design-Iconic-Round.ttf -------------------------------------------------------------------------------- /fonts/osc.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tsubajashi/mpv-settings/f859a9a3b722ce4b27977934a3798c0a99549852/fonts/osc.ttf -------------------------------------------------------------------------------- /fonts/stats mono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tsubajashi/mpv-settings/f859a9a3b722ce4b27977934a3798c0a99549852/fonts/stats mono.ttf -------------------------------------------------------------------------------- /fonts/stats.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tsubajashi/mpv-settings/f859a9a3b722ce4b27977934a3798c0a99549852/fonts/stats.ttf -------------------------------------------------------------------------------- /fonts/subs.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tsubajashi/mpv-settings/f859a9a3b722ce4b27977934a3798c0a99549852/fonts/subs.ttf -------------------------------------------------------------------------------- /input.conf: -------------------------------------------------------------------------------- 1 | WHEEL_UP add volume 2 2 | WHEEL_DOWN add volume -2 3 | UP add volume 2 4 | DOWN add volume -2 5 | AXIS_UP add volume 2 6 | AXIS_DOWN add volume -2 7 | Ctrl+RIGHT seek 85 exact 8 | Ctrl+LEFT seek -85 exact 9 | Shift+RIGHT frame-step 10 | Shift+LEFT frame-back-step 11 | H seek -65 12 | h seek 65 13 | v cycle deband 14 | a cycle audio 15 | s cycle sub 16 | i cycle interpolation 17 | t script-message-to seek_to toggle-seeker 18 | + add audio-delay 0.010 19 | - add audio-delay -0.010 20 | F1 add sub-delay -0.1 21 | F2 add sub-delay +0.1 22 | F4 cycle-values video-aspect-override "16:9" "4:3" "2.35:1" "-1" 23 | F script-binding quality_menu/video_formats_toggle 24 | Alt+f script-binding quality_menu/audio_formats_toggle 25 | Ctrl+r script-binding quality_menu/reload 26 | 27 | 28 | Meta+v script-message-to Mac_Integration OpenFromClipboard 29 | TAB script-message-to Mac_Integration ShowFinder 30 | Ctrl+f script-message-to Mac_Integration ShowInFinder 31 | 32 | # Zoom 33 | - add video-zoom -.25 34 | + add video-zoom .25 35 | 36 | kp8 add video-pan-y .05 37 | kp6 add video-pan-x -.05 38 | kp2 add video-pan-y -.05 39 | kp4 add video-pan-x .05 40 | 41 | kp5 set video-pan-x 0; set video-pan-y 0; set video-zoom 0 42 | # Toggle Playlist with modernx.lua 43 | p script-binding modernx_toggle_playlist 44 | -------------------------------------------------------------------------------- /mpv_linux.conf: -------------------------------------------------------------------------------- 1 | ########### 2 | # GPU API # 3 | ########### 4 | # Controls which type of graphics APIs will be accepted, switch to "d3d11" (on Windows) or "opengl" if you have issues 5 | # Uncomment one API only 6 | 7 | ###### Vulkan Linux, Windows (preferred) 8 | gpu-api=vulkan 9 | 10 | ###### DirectX on Windows 11 | # gpu-api=d3d11 12 | 13 | ###### OpenGL on Linux or macOS or Windows 14 | # gpu-api=opengl 15 | 16 | ########## 17 | # Player # 18 | ########## 19 | 20 | #input-ipc-server=/tmp/mpvsocket 21 | hr-seek-framedrop=no 22 | no-resume-playback 23 | border=no # recommended for ModernX OSC 24 | msg-color=yes 25 | msg-module=yes 26 | 27 | ###### General 28 | # fullscreen=yes # Always open the video player in full screen 29 | # keep-open=yes # Don't close the player after finishing the video 30 | autofit=85%x85% # Start mpv with a % smaller resolution of your screen 31 | cursor-autohide=100 # Cursor hide in ms 32 | 33 | ############### 34 | # Screenshots # 35 | ############### 36 | 37 | screenshot-template="%x/Screens/Screenshot-%F-T%wH.%wM.%wS.%wT-F%{estimated-frame-number}" 38 | screenshot-format=png # Set screenshot format 39 | screenshot-png-compression=4 # Range is 0 to 10. 0 being no compression. 40 | screenshot-tag-colorspace=yes 41 | screenshot-high-bit-depth=yes # Same output bitdepth as the video 42 | 43 | ########### 44 | # OSC/OSD # 45 | ########### 46 | 47 | osc=no # 'no' required for MordernX OSC 48 | osd-bar=yes # Do not remove/comment if mpv_thumbnail_script_client_osc.lua is being used. 49 | osd-font='Inter Tight Medium' # Set a font for OSC 50 | osd-font-size=30 # Set a font size 51 | osd-color='#CCFFFFFF' # ARGB format 52 | osd-border-color='#DD322640' # ARGB format 53 | osd-bar-align-y=-1 # progress bar y alignment (-1 top, 0 centered, 1 bottom) 54 | osd-border-size=2 # size for osd text and progress bar 55 | osd-bar-h=1 # height of osd bar as a fractional percentage of your screen height 56 | osd-bar-w=60 # width of " " " 57 | 58 | ######## 59 | # Subs # 60 | ######## 61 | 62 | blend-subtitles=no 63 | sub-ass-vsfilter-blur-compat=yes # Backward compatibility for vsfilter fansubs 64 | sub-ass-scale-with-window=no # May have undesired effects with signs being misplaced. 65 | sub-auto=fuzzy # external subs don't have to match the file name exactly to autoload 66 | # sub-gauss=0.6 # Some settings fixing VOB/PGS subtitles (creating blur & changing yellow subs to gray) 67 | sub-file-paths-append=ass # search for external subs in these relative subdirectories 68 | sub-file-paths-append=srt 69 | sub-file-paths-append=sub 70 | sub-file-paths-append=subs 71 | sub-file-paths-append=subtitles 72 | demuxer-mkv-subtitle-preroll=yes # try to correctly show embedded subs when seeking 73 | embeddedfonts=yes # use embedded fonts for SSA/ASS subs 74 | sub-fix-timing=no # do not try to fix gaps (which might make it worse in some cases). Enable if there are scenebleeds. 75 | 76 | # Subs - Forced # 77 | 78 | sub-font=Open Sans SemiBold 79 | sub-font-size=46 80 | sub-blur=0.3 81 | sub-border-color=0.0/0.0/0.0/0.8 82 | sub-border-size=3.2 83 | sub-color=0.9/0.9/0.9/1.0 84 | sub-margin-x=100 85 | sub-margin-y=50 86 | sub-shadow-color=0.0/0.0/0.0/0.25 87 | sub-shadow-offset=0 88 | 89 | ######### 90 | # Audio # 91 | ######### 92 | 93 | volume-max=200 # maximum volume in %, everything above 100 results in amplification 94 | audio-stream-silence # fix audio popping on random seek 95 | audio-file-auto=fuzzy # external audio doesn't has to match the file name exactly to autoload 96 | audio-pitch-correction=yes # automatically insert scaletempo when playing with higher speed 97 | 98 | # Languages # 99 | alang=jpn,jp,eng,en,enUS,en-US,de,ger # Audio language priority 100 | slang=eng,en,und,de,ger,jp,jap # Subtitle language priority 101 | 102 | ################## 103 | # Video Profiles # 104 | ################## 105 | 106 | profile=high-quality # mpv --show-profile=gpu-hq 107 | hwdec=auto-copy # enable hardware decoding, defaults to 'no' 108 | vo=gpu-next # GPU-Next: https://github.com/mpv-player/mpv/wiki/GPU-Next-vs-GPU 109 | 110 | ###### Dither 111 | dither-depth=auto 112 | 113 | ###### Debanding 114 | deband=yes 115 | deband-iterations=4 116 | deband-threshold=35 117 | deband-range=16 118 | deband-grain=4 119 | 120 | ###### Luma up (uncomment one shader line only) See: https://artoriuz.github.io/blog/mpv_upscaling.html 121 | glsl-shader="~~/shaders/ravu-zoom-ar-r3-rgb.hook" # good balance between performance and quality 122 | scale=ewa_lanczos 123 | scale-blur=0.981251 124 | 125 | ###### Luma down (optional, uncomment shader line if your hardware can support it) 126 | dscale=catmull_rom 127 | correct-downscaling=yes 128 | linear-downscaling=no 129 | 130 | ###### Chroma up + down (optional, uncomment one shader line only if your hardware can support it) 131 | # glsl-shader="~~/shaders/JointBilateral.glsl" 132 | # glsl-shader="~~/shaders/FastBilateral.glsl" 133 | glsl-shader="~~/shaders/CfL_Prediction.glsl" 134 | cscale=lanczos 135 | sigmoid-upscaling=yes 136 | 137 | ###### Interpolation 138 | video-sync=display-resample 139 | interpolation=yes 140 | tscale=sphinx 141 | tscale-blur=0.6991556596428412 142 | tscale-radius=1.05 143 | tscale-clamp=0.0 144 | 145 | ###### SDR 146 | tone-mapping=bt.2446a 147 | 148 | ###### HDR 149 | target-colorspace-hint=yes 150 | 151 | ############ 152 | # Playback # 153 | ############ 154 | 155 | deinterlace=no # global reset of deinterlacing to off 156 | 157 | [default] 158 | # apply all luma and chroma upscaling and downscaling settings 159 | # apply motion interpolation 160 | 161 | ############################ 162 | # Protocol Specific Config # 163 | ############################ 164 | 165 | [protocol.http] 166 | hls-bitrate=max # use max quality for HLS streams 167 | cache=yes 168 | no-cache-pause # don't pause when the cache runs low 169 | 170 | [protocol.https] 171 | profile=protocol.http 172 | 173 | [protocol.ytdl] 174 | profile=protocol.http 175 | -------------------------------------------------------------------------------- /mpv_osx.conf: -------------------------------------------------------------------------------- 1 | ########### 2 | # GPU API # 3 | ########### 4 | # Controls which type of graphics APIs will be accepted, switch to "d3d11" (on Windows) or "opengl" if you have issues 5 | # Uncomment one API only 6 | 7 | ###### Vulkan Linux, Windows (preferred) 8 | # gpu-api=vulkan 9 | 10 | ###### DirectX on Windows 11 | # gpu-api=d3d11 12 | 13 | ###### OpenGL on Linux or macOS or Windows 14 | # Note: MacOS devices are currently limited to OpenGL v4.1 (which is deprecated). For iOS/tvOS/MacOS devices, Metal v2 would be preferred but there is not currently a Metal backend. In the future, a workaround may be to use mpv + libplacebo + MoltenVK 15 | gpu-api=opengl 16 | 17 | ########## 18 | # Player # 19 | ########## 20 | 21 | #input-ipc-server=/tmp/mpvsocket 22 | hr-seek-framedrop=no 23 | no-resume-playback 24 | border=no # recommended for ModernX OSC 25 | msg-color=yes 26 | msg-module=yes 27 | 28 | ###### General 29 | # fullscreen=yes # Always open the video player in full screen 30 | # keep-open=yes # Don't close the player after finishing the video 31 | autofit=85%x85% # Start mpv with a % smaller resolution of your screen 32 | cursor-autohide=100 # Cursor hide in ms 33 | 34 | ############### 35 | # Screenshots # 36 | ############### 37 | 38 | screenshot-template="%x/Screens/Screenshot-%F-T%wH.%wM.%wS.%wT-F%{estimated-frame-number}" 39 | screenshot-format=png # Set screenshot format 40 | screenshot-png-compression=4 # Range is 0 to 10. 0 being no compression. 41 | screenshot-tag-colorspace=yes 42 | screenshot-high-bit-depth=yes # Same output bitdepth as the video 43 | 44 | ########### 45 | # OSC/OSD # 46 | ########### 47 | 48 | osc=no # 'no' required for MordernX OSC 49 | osd-bar=yes # Do not remove/comment if mpv_thumbnail_script_client_osc.lua is being used. 50 | osd-font='Inter Tight Medium' # Set a font for OSC 51 | osd-font-size=30 # Set a font size 52 | osd-color='#CCFFFFFF' # ARGB format 53 | osd-border-color='#DD322640' # ARGB format 54 | osd-bar-align-y=-1 # progress bar y alignment (-1 top, 0 centered, 1 bottom) 55 | osd-border-size=2 # size for osd text and progress bar 56 | osd-bar-h=1 # height of osd bar as a fractional percentage of your screen height 57 | osd-bar-w=60 # width of " " " 58 | 59 | ######## 60 | # Subs # 61 | ######## 62 | 63 | blend-subtitles=no 64 | sub-ass-vsfilter-blur-compat=yes # Backward compatibility for vsfilter fansubs 65 | sub-ass-scale-with-window=no # May have undesired effects with signs being misplaced. 66 | sub-auto=fuzzy # external subs don't have to match the file name exactly to autoload 67 | # sub-gauss=0.6 # Some settings fixing VOB/PGS subtitles (creating blur & changing yellow subs to gray) 68 | sub-file-paths-append=ass # search for external subs in these relative subdirectories 69 | sub-file-paths-append=srt 70 | sub-file-paths-append=sub 71 | sub-file-paths-append=subs 72 | sub-file-paths-append=subtitles 73 | demuxer-mkv-subtitle-preroll=yes # try to correctly show embedded subs when seeking 74 | embeddedfonts=yes # use embedded fonts for SSA/ASS subs 75 | sub-fix-timing=no # do not try to fix gaps (which might make it worse in some cases). Enable if there are scenebleeds. 76 | 77 | # Subs - Forced # 78 | 79 | sub-font=Open Sans SemiBold 80 | sub-font-size=46 81 | sub-blur=0.3 82 | sub-border-color=0.0/0.0/0.0/0.8 83 | sub-border-size=3.2 84 | sub-color=0.9/0.9/0.9/1.0 85 | sub-margin-x=100 86 | sub-margin-y=50 87 | sub-shadow-color=0.0/0.0/0.0/0.25 88 | sub-shadow-offset=0 89 | 90 | ######### 91 | # Audio # 92 | ######### 93 | 94 | ao=coreaudio 95 | audio-stream-silence # fix audio popping on random seek 96 | audio-file-auto=fuzzy # external audio doesn't has to match the file name exactly to autoload 97 | audio-pitch-correction=yes # automatically insert scaletempo when playing with higher speed 98 | 99 | # Languages # 100 | alang=jpn,jp,eng,en,enUS,en-US,de,ger # Audio language priority 101 | slang=eng,en,und,de,ger,jp,jap # Subtitle language priority 102 | 103 | ################## 104 | # Video Profiles # 105 | ################## 106 | 107 | profile=high-quality # mpv --show-profile=gpu-hq 108 | vo=gpu-next 109 | gpu-context=macvk 110 | macos-force-dedicated-gpu=yes # deactivates the automatic graphics switching and forces the dedicated GPU. 111 | # hwdec=auto-copy-safe # enable hardware decoding, defaults to 'no' 112 | 113 | # OpenGL settings 114 | opengl-pbo=yes 115 | 116 | ###### Dither 117 | dither-depth=auto 118 | 119 | fbo-format=rgba16f # use with gpu-api=opengl 120 | # fbo-format=rgba16hf # use with gpu-api=vulkan 121 | # fbo-format is not not supported in gpu-next profile 122 | glsl-shaders-clr 123 | 124 | # luma upscaling 125 | scale=ewa_lanczossharp # not supported in gpu-next 126 | 127 | # luma downscaling 128 | dscale=catmull_rom 129 | linear-downscaling=no 130 | 131 | # chroma upscaling and downscaling 132 | cscale=lanczos 133 | sigmoid-upscaling=yes 134 | 135 | ###### Debanding 136 | deband=yes 137 | deband-iterations=1 138 | deband-threshold=20 139 | deband-range=16 140 | 141 | ###### Antiring 142 | scale-antiring=0.7 143 | dscale-antiring=0.7 144 | cscale-antiring=0.7 145 | 146 | ###### Interpolation 147 | video-sync=display-resample 148 | interpolation=yes 149 | tscale=box 150 | tscale-window=quadric 151 | tscale-radius=1.1 152 | tscale-clamp=0.0 153 | 154 | tone-mapping=hable 155 | hdr-compute-peak=yes 156 | 157 | target-prim=apple 158 | 159 | ############ 160 | # Playback # 161 | ############ 162 | 163 | deinterlace=no # global reset of deinterlacing to off 164 | 165 | [default] 166 | # apply all luma and chroma upscaling and downscaling settings 167 | # apply motion interpolation 168 | 169 | [protocol.http] 170 | hls-bitrate=max # use max quality for HLS streams 171 | cache=yes 172 | no-cache-pause # don't pause when the cache runs low 173 | 174 | [protocol.https] 175 | profile=protocol.http 176 | 177 | [protocol.ytdl] 178 | profile=protocol.http 179 | -------------------------------------------------------------------------------- /mpv_windows.conf: -------------------------------------------------------------------------------- 1 | ########### 2 | # GPU API # 3 | ########### 4 | # Controls which type of graphics APIs will be accepted, switch to "d3d11" (on Windows) or "opengl" if you have issues 5 | # Uncomment one API only 6 | 7 | ###### Vulkan Linux, Windows (preferred) 8 | #gpu-api=vulkan 9 | 10 | ###### DirectX on Windows 11 | gpu-api=d3d11 12 | 13 | ###### OpenGL on Linux or macOS or Windows 14 | # gpu-api=opengl 15 | 16 | ########## 17 | # Player # 18 | ########## 19 | 20 | #input-ipc-server=mpvpipe 21 | hr-seek-framedrop=no 22 | no-resume-playback 23 | border=no # recommended for ModernX OSC 24 | msg-color=yes 25 | msg-module=yes 26 | 27 | ###### General 28 | # fullscreen=yes # Always open the video player in full screen 29 | # keep-open=yes # Don't close the player after finishing the video 30 | autofit=85%x85% # Start mpv with a % smaller resolution of your screen 31 | cursor-autohide=100 # Cursor hide in ms 32 | 33 | ############### 34 | # Screenshots # 35 | ############### 36 | 37 | screenshot-template="%x\Screens\Screenshot-%F-T%wH.%wM.%wS.%wT-F%{estimated-frame-number}" 38 | screenshot-format=png # Set screenshot format 39 | screenshot-png-compression=4 # Range is 0 to 10. 0 being no compression. 40 | screenshot-tag-colorspace=yes 41 | screenshot-high-bit-depth=yes # Same output bitdepth as the video 42 | 43 | ########### 44 | # OSC/OSD # 45 | ########### 46 | 47 | osc=no # 'no' required for MordernX OSC 48 | osd-bar=yes # Do not remove/comment if mpv_thumbnail_script_client_osc.lua is being used. 49 | osd-font='Inter Tight Medium' # Set a font for OSC 50 | osd-font-size=30 # Set a font size 51 | osd-color='#CCFFFFFF' # ARGB format 52 | osd-border-color='#DD322640' # ARGB format 53 | osd-bar-align-y=-1 # progress bar y alignment (-1 top, 0 centered, 1 bottom) 54 | osd-border-size=2 # size for osd text and progress bar 55 | osd-bar-h=1 # height of osd bar as a fractional percentage of your screen height 56 | osd-bar-w=60 # width of " " " 57 | 58 | ######## 59 | # Subs # 60 | ######## 61 | 62 | blend-subtitles=no 63 | sub-ass-vsfilter-blur-compat=yes # Backward compatibility for vsfilter fansubs 64 | sub-ass-scale-with-window=no # May have undesired effects with signs being misplaced. 65 | sub-auto=fuzzy # external subs don't have to match the file name exactly to autoload 66 | # sub-gauss=0.6 # Some settings fixing VOB/PGS subtitles (creating blur & changing yellow subs to gray) 67 | sub-file-paths-append=ass # search for external subs in these relative subdirectories 68 | sub-file-paths-append=srt 69 | sub-file-paths-append=sub 70 | sub-file-paths-append=subs 71 | sub-file-paths-append=subtitles 72 | demuxer-mkv-subtitle-preroll=yes # try to correctly show embedded subs when seeking 73 | embeddedfonts=yes # use embedded fonts for SSA/ASS subs 74 | sub-fix-timing=no # do not try to fix gaps (which might make it worse in some cases). Enable if there are scenebleeds. 75 | 76 | # Subs - Forced # 77 | 78 | sub-font=Open Sans SemiBold 79 | sub-font-size=46 80 | sub-blur=0.3 81 | sub-border-color=0.0/0.0/0.0/0.8 82 | sub-border-size=3.2 83 | sub-color=0.9/0.9/0.9/1.0 84 | sub-margin-x=100 85 | sub-margin-y=50 86 | sub-shadow-color=0.0/0.0/0.0/0.25 87 | sub-shadow-offset=0 88 | 89 | ######### 90 | # Audio # 91 | ######### 92 | 93 | volume-max=200 # maximum volume in %, everything above 100 results in amplification 94 | audio-stream-silence # fix audio popping on random seek 95 | audio-file-auto=fuzzy # external audio doesn't has to match the file name exactly to autoload 96 | audio-pitch-correction=yes # automatically insert scaletempo when playing with higher speed 97 | 98 | # Languages # 99 | alang=jpn,jp,eng,en,enUS,en-US,de,ger # Audio language priority 100 | slang=eng,en,und,de,ger,jp,jap # Subtitle language priority 101 | 102 | ################## 103 | # Video Profiles # 104 | ################## 105 | 106 | profile=high-quality # mpv --show-profile=gpu-hq 107 | hwdec=auto-copy # enable hardware decoding, defaults to 'no' 108 | vo=gpu-next # GPU-Next: https://github.com/mpv-player/mpv/wiki/GPU-Next-vs-GPU 109 | 110 | ###### Dither 111 | dither-depth=auto 112 | 113 | ###### Debanding 114 | deband=yes 115 | deband-iterations=4 116 | deband-threshold=35 117 | deband-range=16 118 | deband-grain=4 119 | 120 | ###### Luma up (uncomment one shader line only) See: https://artoriuz.github.io/blog/mpv_upscaling.html 121 | glsl-shader="~~/shaders/ravu-zoom-ar-r3-rgb.hook" # good balance between performance and quality 122 | #scale=ewa_lanczos 123 | #scale-blur=0.981251 124 | 125 | ###### Luma down 126 | #dscale=catmull_rom 127 | #correct-downscaling=yes 128 | #linear-downscaling=no 129 | 130 | ###### Chroma up + down (optional, uncomment one shader line only if your hardware can support it) 131 | # glsl-shader="~~/shaders/JointBilateral.glsl" 132 | # glsl-shader="~~/shaders/FastBilateral.glsl" 133 | glsl-shader="~~/shaders/CfL_Prediction.glsl" 134 | #cscale=lanczos 135 | #sigmoid-upscaling=yes 136 | 137 | ###### Antiring 138 | # scale-antiring=0.7 139 | # dscale-antiring=0.7 140 | # cscale-antiring=0.7 141 | 142 | ###### Interpolation 143 | video-sync=display-resample 144 | interpolation=yes 145 | tscale=sphinx 146 | tscale-blur=0.6991556596428412 147 | tscale-radius=1.05 148 | tscale-clamp=0.0 149 | 150 | ###### SDR 151 | tone-mapping=bt.2446a 152 | 153 | ###### HDR 154 | target-colorspace-hint=yes 155 | 156 | ############ 157 | # Playback # 158 | ############ 159 | 160 | deinterlace=no # global reset of deinterlacing to off 161 | 162 | [default] 163 | # apply all luma and chroma upscaling and downscaling settings 164 | # apply motion interpolation 165 | 166 | [protocol.http] 167 | hls-bitrate=max # use max quality for HLS streams 168 | cache=yes 169 | no-cache-pause # don't pause when the cache runs low 170 | 171 | [protocol.https] 172 | profile=protocol.http 173 | 174 | [protocol.ytdl] 175 | profile=protocol.http 176 | -------------------------------------------------------------------------------- /script-opts/playlistmanager.conf: -------------------------------------------------------------------------------- 1 | #### ------- Mpv-Playlistmanager configuration ------- #### 2 | 3 | #### ------- FUNCTIONAL ------- #### 4 | 5 | #navigation keybindings force override only while playlist is visible 6 | #if "no" then you can display the playlist by any of the navigation keys 7 | dynamic_binds=yes 8 | 9 | #dynamic keybind keys, they should not be re-bound in input.conf 10 | #to bind multiple keys separate them by a space 11 | key_moveup=UP 12 | key_movedown=DOWN 13 | key_selectfile=RIGHT LEFT 14 | key_unselectfile= 15 | key_playfile=ENTER 16 | key_removefile=BS 17 | key_closeplaylist=ESC 18 | 19 | #json format for replacing, check .lua for explanation 20 | #example json=[{"ext":{"all":true},"rules":[{"_":" "}]},{"ext":{"mp4":true,"mkv":true},"rules":[{"^(.+)%..+$":"%1"},{"%s*[%[%(].-[%]%)]%s*":""},{"(%w)%.(%w)":"%1 %2"}]},{"protocol":{"http":true,"https":true},"rules":[{"^%a+://w*%.?":""}]}] 21 | #empty for no replace 22 | filename_replace= 23 | 24 | #filetypes to search from directory 25 | loadfiles_filetypes=["jpg","jpeg","png","tif","tiff","gif","webp","svg","bmp","mp3","wav","ogm","flac","m4a","wma","ogg","opus","mkv","avi","mp4","ogv","webm","rmvb","flv","wmv","mpeg","mpg","m4v","3gp"] 26 | 27 | #loadfiles at startup if there is 0 or 1 items in playlist, if 0 uses worḱing dir for files 28 | #requires --idle=yes or --idle=once if 0 files in playlist 29 | loadfiles_on_start=no 30 | 31 | #sort playlist on mpv start 32 | sortplaylist_on_start=no 33 | 34 | #sort playlist when any files are added to playlist after initial load 35 | sortplaylist_on_file_add=no 36 | 37 | #yes: use alphanumerical sort comparison(nonpadded numbers in order), no: use normal lua string comparison 38 | alphanumsort=yes 39 | 40 | #linux | windows | auto 41 | system=auto 42 | 43 | #Use ~ for home directory. Leave as empty to use mpv/playlists 44 | playlist_savepath= 45 | 46 | #save playlist automatically after current file was unloaded 47 | save_playlist_on_file_end=no 48 | 49 | #2 shows playlist, 1 shows current file(filename strip applied), 0 shows nothing 50 | show_playlist_on_fileload=0 51 | 52 | #sync cursor when file is loaded from outside reasons(file-ending, playlist-next shortcut etc.) 53 | sync_cursor_on_load=yes 54 | 55 | #playlist open key will toggle visibility instead of refresh 56 | open_toggles=yes 57 | 58 | #allow the playlist cursor to loop from end to start and vice versa 59 | loop_cursor=yes 60 | 61 | #### ------- VISUAL ------- #### 62 | 63 | #prefer to display titles for following files: "all", "url", "none". Sorting still uses filename 64 | prefer_titles=url 65 | 66 | #call youtube-dl to resolve the titles of urls in the playlist 67 | resolve_titles=no 68 | 69 | #playlist timeout on inactivity, with high value on this open_toggles is good to be yes 70 | playlist_display_timeout=10 71 | 72 | #amount of entries to show before slicing. Optimal value depends on font/video size etc. 73 | showamount=20 74 | 75 | #font size scales by window, if no then needs larger font and padding sizes 76 | scale_playlist_by_window=yes 77 | #playlist ass style overrides 78 | #example {\fnUbuntu\fs10\b0\bord1} equals: font=Ubuntu, size=10, bold=no, border=1 79 | #read http://docs.aegisub.org/3.2/ASS_Tags/ for reference of tags 80 | #no values defaults to OSD settings in mpv.conf 81 | style_ass_tags={\fnInter Tight Medium\fs8\bord0.8} 82 | #paddings for top left corner 83 | text_padding_x=10 84 | text_padding_y=30 85 | 86 | #set title of window with stripped name 87 | set_title_stripped=no 88 | title_prefix= 89 | title_suffix= - mpv 90 | 91 | #slice long filenames, and how many chars to show 92 | slice_longfilenames=no 93 | slice_longfilenames_amount=70 94 | 95 | #Playing header. One newline will be added after the string. 96 | #%mediatitle or %filename = title or name of playing file 97 | #%pos = position of playing file 98 | #%cursor = position of navigation 99 | #%plen = playlist lenght 100 | #%N = newline 101 | playlist_header=[%cursor/%plen] 102 | 103 | #Playlist file templates 104 | #%pos = position of file with leading zeros 105 | #%name = title or name of file 106 | #%N = newline 107 | #you can also use the ass tags mentioned above. For example: 108 | # selected_file={\c&HFF00FF&}➔ %name | to add a color for selected file. However, if you 109 | # use ass tags you need to reset them for every line (see https://github.com/jonniek/mpv-playlistmanager/issues/20) 110 | 111 | normal_file=○ %name 112 | hovered_file=● %name 113 | selected_file=➔ %name 114 | playing_file=▷ %name 115 | playing_hovered_file=▶ %name 116 | playing_selected_file=➤ %name 117 | 118 | #what to show when playlist is truncated 119 | playlist_sliced_prefix=... 120 | playlist_sliced_suffix=... 121 | -------------------------------------------------------------------------------- /script-opts/quality-menu.conf: -------------------------------------------------------------------------------- 1 | # KEY BINDINGS 2 | 3 | # move the menu cursor up 4 | up_binding=UP WHEEL_UP 5 | # move the menu cursor down 6 | down_binding=DOWN WHEEL_DOWN 7 | # select menu entry 8 | select_binding=ENTER MBTN_LEFT 9 | # close menu 10 | close_menu_binding=ESC MBTN_RIGHT F Alt+f 11 | 12 | # youtube-dl version(could be youtube-dl or yt-dlp, or something else) 13 | ytdl_ver=yt-dlp 14 | 15 | # formatting / cursors 16 | selected_and_active=▶ - 17 | selected_and_inactive=● - 18 | unselected_and_active=▷ - 19 | unselected_and_inactive=○ - 20 | 21 | # font size scales by window, if false requires larger font and padding sizes 22 | scale_playlist_by_window=yes 23 | 24 | # playlist ass style overrides inside curly brackets, \keyvalue is one field, extra \ for escape in lua 25 | # example {\\fnUbuntu\\fs10\\b0\\bord1} equals: font=Ubuntu, size=10, bold=no, border=1 26 | # read https://aegi.vmoe.info/docs/3.0/ASS_Tags/ for reference of tags 27 | # undeclared tags will use default osd settings 28 | # these styles will be used for the whole playlist. More specific styling will need to be hacked in 29 | # 30 | # (a monospaced font is recommended but not required) 31 | style_ass_tags={\\fnmonospace\\fs25\\bord1} 32 | 33 | # Shift drawing coordinates. Required for mpv.net compatiblity 34 | shift_x=0 35 | shift_y=0 36 | 37 | # paddings for top left corner 38 | text_padding_x=5 39 | text_padding_y=10 40 | 41 | # Screen dim when menu is open 42 | curtain_opacity=0.7 43 | 44 | # how many seconds until the quality menu times out 45 | # setting this to 0 deactivates the timeout 46 | menu_timeout=6 47 | 48 | # use youtube-dl to fetch a list of available formats (overrides quality_strings) 49 | fetch_formats=yes 50 | 51 | # list of ytdl-format strings to choose from 52 | quality_strings=[ {"4320p" : "bestvideo[height<=?4320p]+bestaudio/best"}, {"2160p" : "bestvideo[height<=?2160]+bestaudio/best"}, {"1440p" : "bestvideo[height<=?1440]+bestaudio/best"}, {"1080p" : "bestvideo[height<=?1080]+bestaudio/best"}, {"720p" : "bestvideo[height<=?720]+bestaudio/best"}, {"480p" : "bestvideo[height<=?480]+bestaudio/best"}, {"360p" : "bestvideo[height<=?360]+bestaudio/best"}, {"240p" : "bestvideo[height<=?240]+bestaudio/best"}, {"144p" : "bestvideo[height<=?144]+bestaudio/best"} ] 53 | 54 | # reset youtube-dl format to the original format string when changing files (e.g. going to the next playlist entry) 55 | # if file was opened previously, reset to previously selected format 56 | reset_format=yes 57 | 58 | # automatically fetch available formats when opening an url 59 | fetch_on_start=yes 60 | 61 | # show the video format menu after opening an url 62 | start_with_menu=no 63 | 64 | # include unknown formats in the list 65 | # Unfortunately choosing which formats are video or audio is not always perfect. 66 | # Set to true to make sure you don't miss any formats, but then the list 67 | # might also include formats that aren't actually video or audio. 68 | # Formats that are known to not be video or audio are still filtered out. 69 | include_unknown=no 70 | 71 | # hide columns that are identical for all formats 72 | hide_identical_columns=yes 73 | 74 | # which columns are shown in which order 75 | # comma separated list, prefix column with "-" to align left 76 | # 77 | # columns that might be useful are: 78 | # resolution, width, height, fps, dynamic_range, tbr, vbr, abr, asr, 79 | # filesize, filesize_approx, vcodec, acodec, ext, video_ext, audio_ext, 80 | # language, format, format_note, quality 81 | # 82 | # columns that are derived from the above, but with special treatment: 83 | # size, frame_rate, bitrate_total, bitrate_video, bitrate_audio, 84 | # codec_video, codec_audio, audio_sample_rate 85 | # 86 | # If those still aren't enough or you're just curious, run: 87 | # yt-dlp -j 88 | # This outputs unformatted JSON. 89 | # Format it and look under "formats" to see what's available. 90 | # 91 | # Not all videos have all columns available. 92 | # Be careful, misspelled columns simply won't be displayed, there is no error. 93 | columns_video=-resolution,frame_rate,dynamic_range,language,bitrate_total,size,-codec_video,-codec_audio 94 | columns_audio=audio_sample_rate,bitrate_total,size,language,-codec_audio 95 | 96 | # columns used for sorting, see "columns_video" for available columns 97 | # comma separated list, prefix column with "-" to reverse sorting order 98 | # Leaving this empty keeps the order from yt-dlp/youtube-dl. 99 | # Be careful, misspelled columns won't result in an error, 100 | # but they might influence the result. 101 | sort_video=height,fps,tbr,size,format_id 102 | sort_audio=asr,tbr,size,format_id 103 | -------------------------------------------------------------------------------- /script-opts/stats.conf: -------------------------------------------------------------------------------- 1 | # MPV - stats.conf 2 | # deus0ww - 2020-01-21 3 | 4 | duration=10 5 | persistent_overlay=yes 6 | filter_params_max_length=0 7 | 8 | plot_perfdata=no 9 | plot_vsync_ratio=no 10 | plot_vsync_jitter=no 11 | 12 | font=Inter Tight Medium 13 | font_mono=JetBrains Mono 14 | font_size=5.5 15 | font_color=fafafa 16 | border_size=0.6 17 | border_color=000000 18 | alpha=11 19 | -------------------------------------------------------------------------------- /scripts/ quality-menu.lua: -------------------------------------------------------------------------------- 1 | -- quality-menu 3.0.1 - 2022-Dec-11 2 | -- https://github.com/christoph-heinrich/mpv-quality-menu 3 | -- 4 | -- Change the stream video and audio quality on the fly. 5 | -- 6 | -- Usage: 7 | -- add bindings to input.conf: 8 | -- F script-binding quality_menu/video_formats_toggle 9 | -- Alt+f script-binding quality_menu/audio_formats_toggle 10 | 11 | local mp = require 'mp' 12 | local utils = require 'mp.utils' 13 | local msg = require 'mp.msg' 14 | local assdraw = require 'mp.assdraw' 15 | local opt = require('mp.options') 16 | 17 | local opts = { 18 | --key bindings 19 | up_binding = "UP WHEEL_UP", 20 | down_binding = "DOWN WHEEL_DOWN", 21 | select_binding = "ENTER MBTN_LEFT", 22 | close_menu_binding = "ESC MBTN_RIGHT F Alt+f", 23 | 24 | --youtube-dl version(could be youtube-dl or yt-dlp, or something else) 25 | ytdl_ver = "yt-dlp", 26 | 27 | --formatting / cursors 28 | selected_and_active = "▶ - ", 29 | selected_and_inactive = "● - ", 30 | unselected_and_active = "▷ - ", 31 | unselected_and_inactive = "○ - ", 32 | 33 | --font size scales by window, if false requires larger font and padding sizes 34 | scale_playlist_by_window = true, 35 | 36 | --playlist ass style overrides inside curly brackets, \keyvalue is one field, extra \ for escape in lua 37 | --example {\\fnUbuntu\\fs10\\b0\\bord1} equals: font=Ubuntu, size=10, bold=no, border=1 38 | --read http://docs.aegisub.org/3.2/ASS_Tags/ for reference of tags 39 | --undeclared tags will use default osd settings 40 | --these styles will be used for the whole playlist. More specific styling will need to be hacked in 41 | -- 42 | --(a monospaced font is recommended but not required) 43 | style_ass_tags = "{\\fnmonospace\\fs25\\bord1}", 44 | 45 | -- Shift drawing coordinates. Required for mpv.net compatiblity 46 | shift_x = 0, 47 | shift_y = 0, 48 | 49 | --paddings from window edge 50 | text_padding_x = 5, 51 | text_padding_y = 10, 52 | 53 | --Screen dim when menu is open 54 | curtain_opacity = 0.7, 55 | 56 | --how many seconds until the quality menu times out 57 | --setting this to 0 deactivates the timeout 58 | menu_timeout = 6, 59 | 60 | --use youtube-dl to fetch a list of available formats (overrides quality_strings) 61 | fetch_formats = true, 62 | 63 | --default menu entries 64 | quality_strings = [[ 65 | [ 66 | {"4320p" : "bestvideo[height<=?4320p]+bestaudio/best"}, 67 | {"2160p" : "bestvideo[height<=?2160]+bestaudio/best"}, 68 | {"1440p" : "bestvideo[height<=?1440]+bestaudio/best"}, 69 | {"1080p" : "bestvideo[height<=?1080]+bestaudio/best"}, 70 | {"720p" : "bestvideo[height<=?720]+bestaudio/best"}, 71 | {"480p" : "bestvideo[height<=?480]+bestaudio/best"}, 72 | {"360p" : "bestvideo[height<=?360]+bestaudio/best"}, 73 | {"240p" : "bestvideo[height<=?240]+bestaudio/best"}, 74 | {"144p" : "bestvideo[height<=?144]+bestaudio/best"} 75 | ] 76 | ]], 77 | 78 | --reset ytdl-format to the original format string when changing files (e.g. going to the next playlist entry) 79 | --if file was opened previously, reset to previously selected format 80 | reset_format = true, 81 | 82 | --automatically fetch available formats when opening an url 83 | fetch_on_start = true, 84 | 85 | --show the video format menu after opening an url 86 | start_with_menu = false, 87 | 88 | --include unknown formats in the list 89 | --Unfortunately choosing which formats are video or audio is not always perfect. 90 | --Set to true to make sure you don't miss any formats, but then the list 91 | --might also include formats that aren't actually video or audio. 92 | --Formats that are known to not be video or audio are still filtered out. 93 | include_unknown = false, 94 | 95 | --hide columns that are identical for all formats 96 | hide_identical_columns = true, 97 | 98 | --which columns are shown in which order 99 | --comma separated list, prefix column with "-" to align left 100 | -- 101 | --columns that might be useful are: 102 | --resolution, width, height, fps, dynamic_range, tbr, vbr, abr, asr, 103 | --filesize, filesize_approx, vcodec, acodec, ext, video_ext, audio_ext, 104 | --language, format, format_note, quality 105 | -- 106 | --columns that are derived from the above, but with special treatment: 107 | --frame_rate, bitrate_total, bitrate_video, bitrate_audio, 108 | --codec_video, codec_audio, audio_sample_rate 109 | -- 110 | --If those still aren't enough or you're just curious, run: 111 | --yt-dlp -j 112 | --This outputs unformatted JSON. 113 | --Format it and look under "formats" to see what's available. 114 | -- 115 | --Not all videos have all columns available. 116 | --Be careful, misspelled columns simply won't be displayed, there is no error. 117 | columns_video = '-resolution,frame_rate,dynamic_range,language,bitrate_total,size,-codec_video,-codec_audio', 118 | columns_audio = 'audio_sample_rate,bitrate_total,size,language,-codec_audio', 119 | 120 | --columns used for sorting, see "columns_video" for available columns 121 | --comma separated list, prefix column with "-" to reverse sorting order 122 | --Leaving this empty keeps the order from yt-dlp/youtube-dl. 123 | --Be careful, misspelled columns won't result in an error, 124 | --but they might influence the result. 125 | sort_video = 'height,fps,tbr,size,format_id', 126 | sort_audio = 'asr,tbr,size,format_id', 127 | } 128 | opt.read_options(opts, "quality-menu") 129 | opts.quality_strings = utils.parse_json(opts.quality_strings) 130 | 131 | opts.font_size = tonumber(opts.style_ass_tags:match('\\fs(%d+%.?%d*)')) or mp.get_property_number('osd-font-size') or 25 132 | opts.curtain_opacity = math.max(math.min(opts.curtain_opacity, 1), 0) 133 | 134 | -- special thanks to reload.lua (https://github.com/4e6/mpv-reload/) 135 | local function reload_resume() 136 | local playlist_pos = mp.get_property_number("playlist-pos") 137 | local reload_duration = mp.get_property_native("duration") 138 | local time_pos = mp.get_property("time-pos") 139 | 140 | mp.set_property_number("playlist-pos", playlist_pos) 141 | 142 | -- Tries to determine live stream vs. pre-recorded VOD. VOD has non-zero 143 | -- duration property. When reloading VOD, to keep the current time position 144 | -- we should provide offset from the start. Stream doesn't have fixed start. 145 | -- Decent choice would be to reload stream from it's current 'live' position. 146 | -- That's the reason we don't pass the offset when reloading streams. 147 | if reload_duration and reload_duration > 0 then 148 | local function seeker() 149 | mp.commandv("seek", time_pos, "absolute") 150 | mp.unregister_event(seeker) 151 | end 152 | 153 | mp.register_event("file-loaded", seeker) 154 | end 155 | end 156 | 157 | local ytdl = { 158 | path = opts.ytdl_ver, 159 | searched = false, 160 | blacklisted = {} 161 | } 162 | 163 | local function process_json(json) 164 | local function is_video(format) 165 | -- "none" means it is not a video 166 | -- nil means it is unknown 167 | return (opts.include_unknown or format.vcodec) and format.vcodec ~= "none" 168 | end 169 | 170 | local function is_audio(format) 171 | return (opts.include_unknown or format.acodec) and format.acodec ~= "none" 172 | end 173 | 174 | local vfmt = nil 175 | local afmt = nil 176 | local requested_formats = json["requested_formats"] or json["requested_downloads"] 177 | for _, format in ipairs(requested_formats) do 178 | if is_video(format) then 179 | vfmt = format["format_id"] 180 | elseif is_audio(format) then 181 | afmt = format["format_id"] 182 | end 183 | end 184 | 185 | local video_formats = {} 186 | local audio_formats = {} 187 | local all_formats = {} 188 | for i = #json.formats, 1, -1 do 189 | local format = json.formats[i] 190 | if is_video(format) then 191 | video_formats[#video_formats + 1] = format 192 | all_formats[#all_formats + 1] = format 193 | elseif is_audio(format) then 194 | audio_formats[#audio_formats + 1] = format 195 | all_formats[#all_formats + 1] = format 196 | end 197 | end 198 | 199 | local function populate_special_fields(format) 200 | format.size = format.filesize or format.filesize_approx 201 | format.frame_rate = format.fps 202 | format.bitrate_total = format.tbr 203 | format.bitrate_video = format.vbr 204 | format.bitrate_audio = format.abr 205 | format.codec_video = format.vcodec 206 | format.codec_audio = format.acodec 207 | format.audio_sample_rate = format.asr 208 | end 209 | 210 | for _, format in ipairs(all_formats) do 211 | populate_special_fields(format) 212 | end 213 | 214 | local function strip_minus(list) 215 | local stripped_list = {} 216 | local had_minus = {} 217 | for i, val in ipairs(list) do 218 | if string.sub(val, 1, 1) == "-" then 219 | val = string.sub(val, 2) 220 | had_minus[val] = true 221 | end 222 | stripped_list[i] = val 223 | end 224 | return stripped_list, had_minus 225 | end 226 | 227 | local function string_split(inputstr, sep) 228 | if sep == nil then 229 | sep = "%s" 230 | end 231 | local t = {} 232 | for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do 233 | table.insert(t, str) 234 | end 235 | return t 236 | end 237 | 238 | local sort_video, reverse_video = strip_minus(string_split(opts.sort_video, ',')) 239 | local sort_audio, reverse_audio = strip_minus(string_split(opts.sort_audio, ',')) 240 | 241 | local function comp(properties, reverse) 242 | return function(a, b) 243 | for _, prop in ipairs(properties) do 244 | local a_val = a[prop] 245 | local b_val = b[prop] 246 | if a_val and b_val and type(a_val) ~= 'table' and a_val ~= b_val then 247 | if reverse[prop] then 248 | return a_val < b_val 249 | else 250 | return a_val > b_val 251 | end 252 | end 253 | end 254 | return false 255 | end 256 | end 257 | 258 | if #sort_video > 0 then 259 | table.sort(video_formats, comp(sort_video, reverse_video)) 260 | end 261 | if #sort_audio > 0 then 262 | table.sort(audio_formats, comp(sort_audio, reverse_audio)) 263 | end 264 | 265 | local function scale_filesize(size) 266 | if size == nil then 267 | return "" 268 | end 269 | size = tonumber(size) 270 | 271 | local counter = 0 272 | while size > 1024 do 273 | size = size / 1024 274 | counter = counter + 1 275 | end 276 | 277 | if counter >= 3 then return string.format("%.1fGiB", size) 278 | elseif counter >= 2 then return string.format("%.1fMiB", size) 279 | elseif counter >= 1 then return string.format("%.1fKiB", size) 280 | else return string.format("%.1fB ", size) 281 | end 282 | end 283 | 284 | local function scale_bitrate(br) 285 | if br == nil then 286 | return "" 287 | end 288 | br = tonumber(br) 289 | 290 | local counter = 0 291 | while br > 1000 do 292 | br = br / 1000 293 | counter = counter + 1 294 | end 295 | 296 | if counter >= 2 then return string.format("%.1fGbps", br) 297 | elseif counter >= 1 then return string.format("%.1fMbps", br) 298 | else return string.format("%.1fKbps", br) 299 | end 300 | end 301 | 302 | local function format_special_fields(format) 303 | local size_prefix = not format.filesize and format.filesize_approx and "~" or "" 304 | format.size = (size_prefix) .. scale_filesize(format.size) 305 | format.frame_rate = format.fps and format.fps .. "fps" or "" 306 | format.bitrate_total = scale_bitrate(format.tbr) 307 | format.bitrate_video = scale_bitrate(format.vbr) 308 | format.bitrate_audio = scale_bitrate(format.abr) 309 | format.codec_video = format.vcodec == nil and "unknown" or format.vcodec == "none" and "" or format.vcodec 310 | format.codec_audio = format.acodec == nil and "unknown" or format.acodec == "none" and "" or format.acodec 311 | format.audio_sample_rate = format.asr and tostring(format.asr) .. "Hz" or "" 312 | end 313 | 314 | for _, format in ipairs(all_formats) do 315 | format_special_fields(format) 316 | end 317 | 318 | local function format_table(formats, columns) 319 | local function calc_shown_columns() 320 | local display_col = {} 321 | local column_widths = {} 322 | local column_values = {} 323 | local columns, column_align_left = strip_minus(columns) 324 | 325 | for _, format in pairs(formats) do 326 | for col, prop in ipairs(columns) do 327 | local label = tostring(format[prop] or "") 328 | format[prop] = label 329 | 330 | if not column_widths[col] or column_widths[col] < label:len() then 331 | column_widths[col] = label:len() 332 | end 333 | 334 | column_values[col] = column_values[col] or label 335 | display_col[col] = display_col[col] or (column_values[col] ~= label) 336 | end 337 | end 338 | 339 | local show_columns = {} 340 | for i, width in ipairs(column_widths) do 341 | if width > 0 and not opts.hide_identical_columns or display_col[i] then 342 | local prop = columns[i] 343 | show_columns[#show_columns + 1] = { 344 | prop = prop, 345 | width = width, 346 | align_left = column_align_left[prop] 347 | } 348 | end 349 | end 350 | return show_columns 351 | end 352 | 353 | local show_columns = calc_shown_columns() 354 | 355 | local spacing = 2 356 | local res = {} 357 | for _, f in ipairs(formats) do 358 | local row = '' 359 | for i, column in ipairs(show_columns) do 360 | -- lua errors out with width > 99 ("invalid conversion specification") 361 | local width = math.min(column.width * (column.align_left and -1 or 1), 99) 362 | row = row .. (i > 1 and string.format('%' .. spacing .. 's', '') or '') 363 | .. string.format('%' .. width .. 's', f[column.prop] or "") 364 | end 365 | res[#res + 1] = { label = row:gsub('%s+$', ''), format = f.format_id } 366 | end 367 | return res 368 | end 369 | 370 | local columns_video = string_split(opts.columns_video, ',') 371 | local columns_audio = string_split(opts.columns_audio, ',') 372 | local vres = format_table(video_formats, columns_video) 373 | local ares = format_table(audio_formats, columns_audio) 374 | return vres, ares, vfmt, afmt 375 | end 376 | 377 | local function get_url() 378 | local path = mp.get_property("path") 379 | if not path then return nil end 380 | path = string.gsub(path, "ytdl://", "") -- Strip possible ytdl:// prefix. 381 | 382 | local function is_url(s) 383 | -- adapted the regex from 384 | -- https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url 385 | return nil ~= 386 | string.match(path, 387 | "^[%w]-://[-a-zA-Z0-9@:%._\\+~#=]+%." .. 388 | "[a-zA-Z0-9()][a-zA-Z0-9()]?[a-zA-Z0-9()]?[a-zA-Z0-9()]?[a-zA-Z0-9()]?[a-zA-Z0-9()]?" .. 389 | "[-a-zA-Z0-9()@:%_\\+.~#?&/=]*") 390 | end 391 | 392 | return is_url(path) and path or nil 393 | end 394 | 395 | local uosc = false 396 | local url_data = {} 397 | local function uosc_set_format_counts() 398 | if not uosc then return end 399 | 400 | local new_path = get_url() 401 | if not new_path then return end 402 | 403 | local data = url_data[new_path] 404 | if data then 405 | mp.commandv('script-message-to', 'uosc', 'set', 'vformats', #data.voptions) 406 | mp.commandv('script-message-to', 'uosc', 'set', 'aformats', #data.aoptions) 407 | else 408 | mp.commandv('script-message-to', 'uosc', 'set', 'vformats', 0) 409 | mp.commandv('script-message-to', 'uosc', 'set', 'aformats', 0) 410 | end 411 | end 412 | 413 | local function process_json_string(url, json) 414 | local json, err = utils.parse_json(json) 415 | 416 | if (json == nil) then 417 | mp.osd_message("fetching formats failed...", 2) 418 | if err == nil then err = "unexpected error occurred" end 419 | msg.error("failed to parse JSON data: " .. err) 420 | return 421 | end 422 | 423 | if json.formats == nil then 424 | return 425 | end 426 | 427 | local vres, ares, vfmt, afmt = process_json(json) 428 | url_data[url] = { voptions = vres, aoptions = ares, vfmt = vfmt, afmt = afmt } 429 | uosc_set_format_counts() 430 | return vres, ares, vfmt, afmt 431 | end 432 | 433 | local function download_formats(url) 434 | 435 | if opts.fetch_on_start and not opts.start_with_menu then 436 | msg.info("fetching available formats with youtube-dl...") 437 | else 438 | mp.osd_message("fetching available formats with youtube-dl...", 60) 439 | end 440 | 441 | if not (ytdl.searched) then 442 | local ytdl_mcd = mp.find_config_file(opts.ytdl_ver) 443 | if not (ytdl_mcd == nil) then 444 | msg.verbose("found youtube-dl at: " .. ytdl_mcd) 445 | ytdl.path = ytdl_mcd 446 | end 447 | ytdl.searched = true 448 | end 449 | 450 | local function exec(args) 451 | msg.debug("Running: " .. table.concat(args, " ")) 452 | local ret = mp.command_native({ 453 | name = "subprocess", 454 | args = args, 455 | capture_stdout = true, 456 | capture_stderr = true 457 | }) 458 | return ret.status, ret.stdout, ret, ret.killed_by_us 459 | end 460 | 461 | local function check_version(ytdl_path) 462 | local command = { 463 | name = "subprocess", 464 | capture_stdout = true, 465 | args = { ytdl_path, "--version" } 466 | } 467 | local version_string = mp.command_native(command).stdout 468 | local year, month, day = string.match(version_string, "(%d+).(%d+).(%d+)") 469 | 470 | -- sanity check 471 | if (tonumber(year) < 2000) or (tonumber(month) > 12) or 472 | (tonumber(day) > 31) then 473 | return 474 | end 475 | local version_ts = os.time { year = year, month = month, day = day } 476 | if (os.difftime(os.time(), version_ts) > 60 * 60 * 24 * 90) then 477 | msg.warn("It appears that your youtube-dl version is severely out of date.") 478 | end 479 | end 480 | 481 | local ytdl_format = mp.get_property("ytdl-format") 482 | local command = nil 483 | if (ytdl_format == nil or ytdl_format == "") then 484 | command = { ytdl.path, "--no-warnings", "--no-playlist", "-J", url } 485 | else 486 | command = { ytdl.path, "--no-warnings", "--no-playlist", "-J", "-f", ytdl_format, url } 487 | end 488 | 489 | msg.verbose("calling youtube-dl with command: " .. table.concat(command, " ")) 490 | 491 | local es, json, result, aborted = exec(command) 492 | 493 | if aborted then 494 | return 495 | end 496 | 497 | if (es ~= 0) or (json == "") then 498 | json = nil 499 | end 500 | 501 | if (json == nil) then 502 | mp.osd_message("fetching formats failed...", 2) 503 | msg.verbose("status:", es) 504 | msg.verbose("reason:", result.error_string) 505 | msg.verbose("stdout:", result.stdout) 506 | msg.verbose("stderr:", result.stderr) 507 | 508 | -- trim our stderr to avoid spurious newlines 509 | local ytdl_err = result.stderr:gsub("^%s*(.-)%s*$", "%1") 510 | msg.error(ytdl_err) 511 | local err = "youtube-dl failed: " 512 | if result.error_string and result.error_string == "init" then 513 | err = err .. "not found or not enough permissions" 514 | elseif not result.killed_by_us then 515 | err = err .. "unexpected error occurred" 516 | else 517 | err = string.format("%s returned '%d'", err, es) 518 | end 519 | msg.error(err) 520 | if string.find(ytdl_err, "yt%-dl%.org/bug") then 521 | check_version(ytdl.path) 522 | end 523 | return 524 | end 525 | 526 | msg.verbose("youtube-dl succeeded!") 527 | mp.osd_message("", 0) 528 | 529 | local vres, ares, vfmt, afmt = process_json_string(url, json) 530 | return vres, ares, vfmt, afmt 531 | end 532 | 533 | local function send_formats_to(type, url, script_name, options, format_id) 534 | mp.commandv('script-message-to', script_name, type .. '_formats', 535 | url, utils.format_json(options or {}), format_id or '') 536 | end 537 | 538 | local queue_callback_video = {} 539 | local queue_callback_audio = {} 540 | local function get_formats() 541 | 542 | local url = get_url() 543 | if url == nil then 544 | return 545 | end 546 | 547 | if url_data[url] then 548 | local data = url_data[url] 549 | return data.voptions, data.aoptions, data.vfmt, data.afmt, url 550 | end 551 | 552 | if opts.fetch_formats == false then 553 | local vres = {} 554 | for i, v in ipairs(opts.quality_strings) do 555 | for k, v2 in pairs(v) do 556 | vres[i] = { label = k, format = v2 } 557 | end 558 | end 559 | url_data[url] = { voptions = vres, aoptions = {}, vfmt = nil, afmt = nil } 560 | return vres, {}, nil, nil, url 561 | end 562 | 563 | local vres, ares, vfmt, afmt = download_formats(url) 564 | 565 | for _, script_name in ipairs(queue_callback_video[url] or {}) do 566 | send_formats_to('video', url, script_name, vres, vfmt) 567 | end 568 | for _, script_name in ipairs(queue_callback_audio[url] or {}) do 569 | send_formats_to('audio', url, script_name, ares, afmt) 570 | end 571 | 572 | queue_callback_video[url] = nil 573 | queue_callback_audio[url] = nil 574 | return vres, ares, vfmt, afmt, url 575 | end 576 | 577 | local function format_string(vfmt, afmt) 578 | if vfmt and afmt then 579 | return vfmt .. "+" .. afmt 580 | elseif vfmt then 581 | return vfmt 582 | elseif afmt then 583 | return afmt 584 | else 585 | return "" 586 | end 587 | end 588 | 589 | local function set_format(url, vfmt, afmt) 590 | if (url_data[url].vfmt ~= vfmt or url_data[url].afmt ~= afmt) then 591 | url_data[url].afmt = afmt 592 | url_data[url].vfmt = vfmt 593 | if url == mp.get_property("path") then 594 | mp.set_property("ytdl-format", format_string(vfmt, afmt)) 595 | reload_resume() 596 | end 597 | end 598 | end 599 | 600 | local destroyer = nil 601 | local function show_menu(isvideo) 602 | 603 | if destroyer then 604 | destroyer() 605 | end 606 | 607 | local voptions, aoptions, vfmt, afmt, url = get_formats() 608 | 609 | local options 610 | local fmt 611 | if isvideo then 612 | options = voptions 613 | fmt = vfmt 614 | else 615 | options = aoptions 616 | fmt = afmt 617 | end 618 | 619 | if options == nil then 620 | if uosc then 621 | if isvideo then 622 | mp.commandv('script-binding', 'uosc/video') 623 | else 624 | mp.commandv('script-binding', 'uosc/audio') 625 | end 626 | end 627 | 628 | return 629 | end 630 | 631 | msg.verbose("current ytdl-format: " .. format_string(vfmt, afmt)) 632 | 633 | local active = 0 634 | local selected = 1 635 | --set the cursor to the current format 636 | if fmt then 637 | for i, v in ipairs(options) do 638 | if v.format == fmt then 639 | active = i 640 | selected = active 641 | break 642 | end 643 | end 644 | else 645 | active = #options + 1 646 | selected = active 647 | end 648 | 649 | if uosc then 650 | local menu = { 651 | title = isvideo and 'Video Formats' or 'Audio Formats', 652 | items = {}, 653 | type = (isvideo and 'video' or 'audio') .. '_formats', 654 | } 655 | for i, option in ipairs(options) do 656 | menu.items[i] = { 657 | title = option.label, 658 | active = i == active, 659 | value = { 660 | 'script-message-to', 661 | 'quality_menu', 662 | (isvideo and 'video' or 'audio') .. '-format-set', 663 | url, 664 | option.format 665 | } 666 | } 667 | end 668 | menu.items[#menu.items + 1] = { 669 | title = 'None', 670 | value = { 671 | 'script-message-to', 672 | 'quality_menu', 673 | (isvideo and 'video' or 'audio') .. '-format-set', 674 | url 675 | } 676 | } 677 | local json = utils.format_json(menu) 678 | mp.commandv('script-message-to', 'uosc', 'open-menu', json) 679 | return 680 | end 681 | 682 | local function choose_prefix(i) 683 | if i == selected and i == active then return opts.selected_and_active 684 | elseif i == selected then return opts.selected_and_inactive end 685 | 686 | if i ~= selected and i == active then return opts.unselected_and_active 687 | elseif i ~= selected then return opts.unselected_and_inactive end 688 | return "> " --shouldn't get here. 689 | end 690 | 691 | local width, height 692 | local margin_top, margin_bottom = 0, 0 693 | local num_options = #options + 1 694 | 695 | local function get_scrolled_lines() 696 | local output_height = height - opts.text_padding_y * 2 - margin_top * height - margin_bottom * height 697 | local screen_lines = math.max(math.floor(output_height / opts.font_size), 1) 698 | local max_scroll = math.max(num_options - screen_lines, 0) 699 | return math.min(math.max(selected - math.ceil(screen_lines / 2), 0), max_scroll) 700 | end 701 | 702 | local function draw_menu() 703 | local ass = assdraw.ass_new() 704 | 705 | if opts.curtain_opacity > 0 then 706 | local alpha = 255 - math.ceil(255 * opts.curtain_opacity) 707 | ass.text = string.format('{\\pos(0,0)\\rDefault\\an7\\1c&H000000&\\alpha&H%X&}', alpha) 708 | ass:draw_start() 709 | ass:rect_cw(0, 0, width, height) 710 | ass:draw_stop() 711 | ass:new_event() 712 | end 713 | 714 | local scrolled_lines = get_scrolled_lines() 715 | local pos_y = opts.shift_y + margin_top * height + opts.text_padding_y - scrolled_lines * opts.font_size 716 | ass:pos(opts.shift_x + opts.text_padding_x, pos_y) 717 | local clip_top = math.floor(margin_top * height + 0.5) 718 | local clip_bottom = math.floor((1 - margin_bottom) * height + 0.5) 719 | local clipping_coordinates = '0,' .. clip_top .. ',' .. width .. ',' .. clip_bottom 720 | ass:append(opts.style_ass_tags .. '{\\q2\\clip(' .. clipping_coordinates .. ')}') 721 | 722 | if #options > 0 then 723 | for i, v in ipairs(options) do 724 | ass:append(choose_prefix(i) .. v.label .. "\\N") 725 | end 726 | ass:append(choose_prefix(#options + 1) .. "None") 727 | else 728 | ass:append("no formats found") 729 | end 730 | 731 | mp.set_osd_ass(width, height, ass.text) 732 | end 733 | 734 | local function update_dimensions() 735 | local _, h, aspect = mp.get_osd_size() 736 | if opts.scale_playlist_by_window then h = 720 end 737 | height = h 738 | width = h * aspect 739 | draw_menu() 740 | end 741 | 742 | local function update_margins() 743 | local shared_props = mp.get_property_native('shared-script-properties') 744 | local val = shared_props['osc-margins'] 745 | if val then 746 | -- formatted as "%f,%f,%f,%f" with left, right, top, bottom, each 747 | -- value being the border size as ratio of the window size (0.0-1.0) 748 | local vals = {} 749 | for v in string.gmatch(val, "[^,]+") do 750 | vals[#vals + 1] = tonumber(v) 751 | end 752 | margin_top = vals[3] -- top 753 | margin_bottom = vals[4] -- bottom 754 | else 755 | margin_top = 0 756 | margin_bottom = 0 757 | end 758 | draw_menu() 759 | end 760 | 761 | update_dimensions() 762 | update_margins() 763 | mp.observe_property('osd-dimensions', 'native', update_dimensions) 764 | mp.observe_property('shared-script-properties', 'native', update_margins) 765 | 766 | local timeout = nil 767 | 768 | local function selected_move(amt) 769 | selected = selected + amt 770 | if selected < 1 then selected = num_options 771 | elseif selected > num_options then selected = 1 end 772 | if timeout then 773 | timeout:kill() 774 | timeout:resume() 775 | end 776 | draw_menu() 777 | end 778 | 779 | local function bind_keys(keys, name, func, opts) 780 | if not keys then 781 | mp.add_forced_key_binding(keys, name, func, opts) 782 | return 783 | end 784 | local i = 1 785 | for key in keys:gmatch("[^%s]+") do 786 | local prefix = i == 1 and '' or i 787 | mp.add_forced_key_binding(key, name .. prefix, func, opts) 788 | i = i + 1 789 | end 790 | end 791 | 792 | local function unbind_keys(keys, name) 793 | if not keys then 794 | mp.remove_key_binding(name) 795 | return 796 | end 797 | local i = 1 798 | for key in keys:gmatch("[^%s]+") do 799 | local prefix = i == 1 and '' or i 800 | mp.remove_key_binding(name .. prefix) 801 | i = i + 1 802 | end 803 | end 804 | 805 | local function destroy() 806 | if timeout then 807 | timeout:kill() 808 | end 809 | mp.set_osd_ass(0, 0, "") 810 | unbind_keys(opts.up_binding, "move_up") 811 | unbind_keys(opts.down_binding, "move_down") 812 | unbind_keys(opts.select_binding, "select") 813 | unbind_keys(opts.close_menu_binding, "close") 814 | mp.unobserve_property(update_dimensions) 815 | mp.unobserve_property(update_margins) 816 | destroyer = nil 817 | end 818 | 819 | if opts.menu_timeout > 0 then 820 | timeout = mp.add_periodic_timer(opts.menu_timeout, destroy) 821 | end 822 | destroyer = destroy 823 | 824 | bind_keys(opts.up_binding, "move_up", function() selected_move(-1) end, { repeatable = true }) 825 | bind_keys(opts.down_binding, "move_down", function() selected_move(1) end, { repeatable = true }) 826 | if #options > 0 then 827 | bind_keys(opts.select_binding, "select", function() 828 | destroy() 829 | if selected == active then return end 830 | 831 | fmt = options[selected] and options[selected].format or nil 832 | if isvideo then 833 | vfmt = fmt 834 | else 835 | afmt = fmt 836 | end 837 | set_format(url, vfmt, afmt) 838 | end) 839 | end 840 | bind_keys(opts.close_menu_binding, "close", destroy) --close menu using ESC 841 | mp.osd_message("", 0) 842 | draw_menu() 843 | end 844 | 845 | local ui_callback = {} 846 | 847 | local function video_formats_toggle() 848 | if #ui_callback > 0 then 849 | for _, name in ipairs(ui_callback) do 850 | mp.commandv('script-message-to', name, 'video-formats-menu') 851 | end 852 | else 853 | show_menu(true) 854 | end 855 | end 856 | 857 | local function audio_formats_toggle() 858 | if #ui_callback > 0 then 859 | for _, name in ipairs(ui_callback) do 860 | mp.commandv('script-message-to', name, 'audio-formats-menu') 861 | end 862 | else 863 | show_menu(false) 864 | end 865 | end 866 | 867 | -- keybind to launch menu 868 | mp.add_key_binding(nil, "video_formats_toggle", video_formats_toggle) 869 | mp.add_key_binding(nil, "audio_formats_toggle", audio_formats_toggle) 870 | mp.add_key_binding(nil, "reload", reload_resume) 871 | 872 | local original_format = mp.get_property("ytdl-format") 873 | local path = nil 874 | local function file_start() 875 | uosc_set_format_counts() 876 | 877 | local new_path = get_url() 878 | if not new_path then return end 879 | 880 | local data = url_data[new_path] 881 | 882 | if opts.reset_format and path and new_path ~= path then 883 | if data then 884 | msg.verbose("setting previously set format") 885 | mp.set_property("ytdl-format", format_string(data.vfmt, data.afmt)) 886 | else 887 | msg.verbose("setting original format") 888 | mp.set_property("ytdl-format", original_format) 889 | end 890 | end 891 | if opts.start_with_menu and new_path ~= path then 892 | video_formats_toggle() 893 | elseif opts.fetch_on_start and not data then 894 | download_formats(new_path) 895 | end 896 | path = new_path 897 | end 898 | 899 | mp.register_event("start-file", file_start) 900 | 901 | mp.register_script_message('video-formats-get', function(url, script_name) 902 | local data = url_data[url] 903 | if data then 904 | send_formats_to('video', url, script_name, data.voptions, data.vfmt) 905 | else 906 | local queue = queue_callback_video[url] or {} 907 | queue[#queue + 1] = script_name 908 | queue_callback_video[url] = queue 909 | get_formats() 910 | end 911 | end) 912 | 913 | mp.register_script_message('audio-formats-get', function(url, script_name) 914 | local data = url_data[url] 915 | if data then 916 | send_formats_to('audio', url, script_name, data.aoptions, data.afmt) 917 | else 918 | local queue = queue_callback_audio[url] or {} 919 | queue[#queue + 1] = script_name 920 | queue_callback_audio[url] = queue 921 | get_formats() 922 | end 923 | end) 924 | 925 | mp.register_script_message('video-format-set', function(url, format_id) 926 | set_format(url, format_id, url_data[url].afmt) 927 | end) 928 | 929 | mp.register_script_message('audio-format-set', function(url, format_id) 930 | set_format(url, url_data[url].vfmt, format_id) 931 | end) 932 | 933 | mp.register_script_message('register-ui', function(script_name) 934 | ui_callback[#ui_callback + 1] = script_name 935 | end) 936 | 937 | -- check if uosc is running 938 | mp.register_script_message('uosc-version', function(version) 939 | version = tonumber((version:gsub('%.', ''))) 940 | ---@diagnostic disable-next-line: cast-local-type 941 | uosc = version and version >= 400 942 | uosc_set_format_counts() 943 | end) 944 | mp.commandv('script-message-to', 'uosc', 'get-version', mp.get_script_name()) 945 | -------------------------------------------------------------------------------- /scripts/Mac_Integration.lua: -------------------------------------------------------------------------------- 1 | -- deus0ww - 2019-07-01 2 | 3 | local mp = require 'mp' 4 | local msg = require 'mp.msg' 5 | 6 | 7 | 8 | -- Show Finder 9 | mp.register_script_message('ShowFinder', function() 10 | mp.command_native({'run', 'open', '-a', 'Finder'}) 11 | end) 12 | 13 | 14 | 15 | -- Show File in Finder 16 | mp.register_script_message('ShowInFinder', function() 17 | local path = mp.get_property_native('path', '') 18 | msg.debug('Show in Finder:', path) 19 | if path == '' then return end 20 | local cmd = {'open'} 21 | if path:find('http://') ~= nil or path:find('https://') ~= nil then 22 | elseif path:find('edl://') ~= nil then 23 | cmd[#cmd+1] = '-R' 24 | path = path:gsub('edl://', ''):gsub(';/', '" /"') 25 | elseif path:find('file://') ~= nil then 26 | cmd[#cmd+1] = '-R' 27 | path = path:gsub('file://', '') 28 | else 29 | cmd[#cmd+1] = '-R' 30 | end 31 | cmd[#cmd+1] = path 32 | mp.command_native( {name='subprocess', args=cmd} ) 33 | end) 34 | 35 | 36 | 37 | -- Move to Trash -- Requires: https://github.com/ali-rantakari/trash 38 | mp.register_script_message('MoveToTrash', function() 39 | local demux_state = mp.get_property_native('demuxer-cache-state', {}) 40 | local demux_ranges = demux_state['seekable-ranges'] and #demux_state['seekable-ranges'] or 1 41 | if demux_ranges > 0 then 42 | mp.osd_message('Trashing not supported.') 43 | return 44 | end 45 | local path = mp.get_property_native('path', ''):gsub('edl://', ''):gsub(';/', '" /"') 46 | msg.debug('Moving to Trash:', path) 47 | if path and path ~= '' then 48 | mp.command_native({'run', 'trash', '-F', path}) 49 | mp.osd_message('Trashed.') 50 | else 51 | mp.osd_message('Trashing failed.') 52 | end 53 | end) 54 | 55 | 56 | 57 | -- Open From Clipboard - One URL per line 58 | mp.register_script_message('OpenFromClipboard', function() 59 | local osd_msg = 'Opening From Clipboard: ' 60 | 61 | local success, result = pcall(io.popen, 'pbpaste') 62 | if not success or not result then 63 | mp.osd_message(osd_msg .. 'n/a') 64 | return 65 | end 66 | local lines = {} 67 | for line in result:lines() do lines[#lines+1] = line end 68 | if #lines == 0 then 69 | mp.osd_message(osd_msg .. 'n/a') 70 | return 71 | end 72 | 73 | local mode = 'replace' 74 | for _, line in ipairs(lines) do 75 | msg.debug('loadfile', line, mode) 76 | mp.commandv('loadfile', line, mode) 77 | mode = 'append' 78 | end 79 | 80 | local msg = osd_msg 81 | if #lines > 0 then msg = msg .. '\n' .. lines[1] end 82 | if #lines > 1 then msg = msg .. (' ... and %d other URL(s).'):format(#lines-1) end 83 | mp.osd_message(msg, 6.0) 84 | end) 85 | -------------------------------------------------------------------------------- /scripts/acompressor.lua: -------------------------------------------------------------------------------- 1 | -- This script adds control to the dynamic range compression ffmpeg 2 | -- filter including key bindings for adjusting parameters. 3 | -- 4 | -- See https://ffmpeg.org/ffmpeg-filters.html#acompressor for explanation 5 | -- of the parameters. 6 | 7 | local mp = require 'mp' 8 | local options = require 'mp.options' 9 | 10 | local o = { 11 | default_enable = false, 12 | show_osd = true, 13 | osd_timeout = 4000, 14 | filter_label = mp.get_script_name(), 15 | 16 | key_toggle = 'n', 17 | key_increase_threshold = 'F1', 18 | key_decrease_threshold = 'Shift+F1', 19 | key_increase_ratio = 'F2', 20 | key_decrease_ratio = 'Shift+F2', 21 | key_increase_knee = 'F3', 22 | key_decrease_knee = 'Shift+F3', 23 | key_increase_makeup = 'F4', 24 | key_decrease_makeup = 'Shift+F4', 25 | key_increase_attack = 'F5', 26 | key_decrease_attack = 'Shift+F5', 27 | key_increase_release = 'F6', 28 | key_decrease_release = 'Shift+F6', 29 | 30 | default_threshold = -25.0, 31 | default_ratio = 3.0, 32 | default_knee = 2.0, 33 | default_makeup = 8.0, 34 | default_attack = 20.0, 35 | default_release = 250.0, 36 | 37 | step_threshold = -2.5, 38 | step_ratio = 1.0, 39 | step_knee = 1.0, 40 | step_makeup = 1.0, 41 | step_attack = 10.0, 42 | step_release = 10.0, 43 | } 44 | options.read_options(o) 45 | 46 | local params = { 47 | { name = 'attack', min=0.01, max=2000, hide_default=true, dB='' }, 48 | { name = 'release', min=0.01, max=9000, hide_default=true, dB='' }, 49 | { name = 'threshold', min= -30, max= 0, hide_default=false, dB='dB' }, 50 | { name = 'ratio', min= 1, max= 20, hide_default=false, dB='' }, 51 | { name = 'knee', min= 1, max= 10, hide_default=true, dB='dB' }, 52 | { name = 'makeup', min= 0, max= 24, hide_default=false, dB='dB' }, 53 | } 54 | 55 | local function parse_value(value) 56 | -- 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. 57 | -- And we can't omit it because gsub returns 2 values which would get unpacked and cause more problems. Gotta love scripting languages. 58 | return tonumber(value:gsub('dB$', ''), nil) 59 | end 60 | 61 | local function format_value(value, dB) 62 | return string.format('%g%s', value, dB) 63 | end 64 | 65 | local function show_osd(filter) 66 | if not o.show_osd then 67 | return 68 | end 69 | 70 | if not filter.enabled then 71 | mp.commandv('show-text', 'Dynamic range compressor: disabled', o.osd_timeout) 72 | return 73 | end 74 | 75 | local pretty = {} 76 | for _,param in ipairs(params) do 77 | local value = parse_value(filter.params[param.name]) 78 | if not (param.hide_default and value == o['default_' .. param.name]) then 79 | pretty[#pretty+1] = string.format('%s: %g%s', param.name:gsub("^%l", string.upper), value, param.dB) 80 | end 81 | end 82 | 83 | if #pretty == 0 then 84 | pretty = '' 85 | else 86 | pretty = '\n(' .. table.concat(pretty, ', ') .. ')' 87 | end 88 | 89 | mp.commandv('show-text', 'Dynamic range compressor: enabled' .. pretty, o.osd_timeout) 90 | end 91 | 92 | local function get_filter() 93 | local af = mp.get_property_native('af', {}) 94 | 95 | for i = 1, #af do 96 | if af[i].label == o.filter_label then 97 | return af, i 98 | end 99 | end 100 | 101 | af[#af+1] = { 102 | name = 'acompressor', 103 | label = o.filter_label, 104 | enabled = false, 105 | params = {}, 106 | } 107 | 108 | for _,param in pairs(params) do 109 | af[#af].params[param.name] = format_value(o['default_' .. param.name], param.dB) 110 | end 111 | 112 | return af, #af 113 | end 114 | 115 | local function toggle_acompressor() 116 | local af, i = get_filter() 117 | af[i].enabled = not af[i].enabled 118 | mp.set_property_native('af', af) 119 | show_osd(af[i]) 120 | end 121 | 122 | local function update_param(name, increment) 123 | for _,param in pairs(params) do 124 | if param.name == string.lower(name) then 125 | local af, i = get_filter() 126 | local value = parse_value(af[i].params[param.name]) 127 | value = math.max(param.min, math.min(value + increment, param.max)) 128 | af[i].params[param.name] = format_value(value, param.dB) 129 | af[i].enabled = true 130 | mp.set_property_native('af', af) 131 | show_osd(af[i]) 132 | return 133 | end 134 | end 135 | 136 | mp.msg.error('Unknown parameter "' .. name .. '"') 137 | end 138 | 139 | mp.add_key_binding(o.key_toggle, "toggle-acompressor", toggle_acompressor) 140 | mp.register_script_message('update-param', update_param) 141 | 142 | for _,param in pairs(params) do 143 | for direction,step in pairs({increase=1, decrease=-1}) do 144 | mp.add_key_binding(o['key_' .. direction .. '_' .. param.name], 145 | 'acompressor-' .. direction .. '-' .. param.name, 146 | function() update_param(param.name, step*o['step_' .. param.name]); end, 147 | { repeatable = true }) 148 | end 149 | end 150 | 151 | if o.default_enable then 152 | local af, i = get_filter() 153 | af[i].enabled = true 154 | mp.set_property_native('af', af) 155 | end 156 | -------------------------------------------------------------------------------- /scripts/appendURL.lua: -------------------------------------------------------------------------------- 1 | -- appendurl - Tsubajashi 2 | 3 | local platform = nil --set to 'linux', 'windows' or 'macos' to override automatic assign 4 | 5 | if not platform then 6 | local o = {} 7 | if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then 8 | platform = 'windows' 9 | elseif mp.get_property_native('options/input-app-events', o) ~= o then 10 | platform = 'macos' 11 | else 12 | platform = 'linux' 13 | end 14 | end 15 | 16 | local utils = require 'mp.utils' 17 | local msg = require 'mp.msg' 18 | 19 | --main function 20 | function append(primaryselect) 21 | local clipboard = get_clipboard(primaryselect or false) 22 | if clipboard then 23 | mp.commandv("loadfile", clipboard, "append-play") 24 | mp.osd_message("URL appended: "..clipboard) 25 | msg.info("URL appended: "..clipboard) 26 | end 27 | end 28 | 29 | --handles the subprocess response table and return clipboard if it was a success 30 | function handleres(res, args, primary) 31 | if not res.error and res.status == 0 then 32 | return res.stdout 33 | else 34 | --if clipboard failed try primary selection 35 | if platform=='linux' and not primary then 36 | append(true) 37 | return nil 38 | end 39 | msg.error("There was an error getting "..platform.." clipboard: ") 40 | msg.error(" Status: "..(res.status or "")) 41 | msg.error(" Error: "..(res.error or "")) 42 | msg.error(" stdout: "..(res.stdout or "")) 43 | msg.error("args: "..utils.to_string(args)) 44 | return nil 45 | end 46 | end 47 | 48 | function get_clipboard(primary) 49 | if platform == 'linux' then 50 | local args = { 'xclip', '-selection', primary and 'primary' or 'clipboard', '-out' } 51 | return handleres(utils.subprocess({ args = args }), args, primary) 52 | elseif platform == 'windows' then 53 | local args = { 54 | 'powershell', '-NoProfile', '-Command', [[& { 55 | Trap { 56 | Write-Error -ErrorRecord $_ 57 | Exit 1 58 | } 59 | 60 | $clip = "" 61 | if (Get-Command "Get-Clipboard" -errorAction SilentlyContinue) { 62 | $clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText 63 | } else { 64 | Add-Type -AssemblyName PresentationCore 65 | $clip = [Windows.Clipboard]::GetText() 66 | } 67 | 68 | $clip = $clip -Replace "`r","" 69 | $u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip) 70 | [Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length) 71 | }]] 72 | } 73 | return handleres(utils.subprocess({ args = args }), args) 74 | elseif platform == 'macos' then 75 | local args = { 'pbpaste' } 76 | return handleres(utils.subprocess({ args = args }), args) 77 | end 78 | return nil 79 | end 80 | 81 | mp.add_key_binding("ctrl+v", "appendURL", append) -------------------------------------------------------------------------------- /scripts/audio-osc.lua: -------------------------------------------------------------------------------- 1 | -- show osc all the time when file is an audio file. 2 | -- source: https://github.com/mpv-player/mpv/issues/3500#issuecomment-305646994 3 | 4 | mp.register_event("file-loaded", function() 5 | local hasvid = mp.get_property_osd("video") ~= "no" 6 | mp.commandv("script-message", "osc-visibility", (hasvid and "auto" or "always"), "no-osd") 7 | -- remove the next line if you don't want to affect the osd-bar config 8 | mp.commandv("set", "options/osd-bar", (hasvid and "yes" or "no")) 9 | end) -------------------------------------------------------------------------------- /scripts/autoload.lua: -------------------------------------------------------------------------------- 1 | -- This script automatically loads playlist entries before and after the 2 | -- the currently played file. It does so by scanning the directory a file is 3 | -- located in when starting playback. It sorts the directory entries 4 | -- alphabetically, and adds entries before and after the current file to 5 | -- the internal playlist. (It stops if it would add an already existing 6 | -- playlist entry at the same position - this makes it "stable".) 7 | -- Add at most 5000 * 2 files when starting a file (before + after). 8 | 9 | --[[ 10 | To configure this script use file autoload.conf in directory script-opts (the "script-opts" 11 | directory must be in the mpv configuration directory, typically ~/.config/mpv/). 12 | 13 | Example configuration would be: 14 | 15 | disabled=no 16 | images=no 17 | videos=yes 18 | audio=yes 19 | ignore_hidden=yes 20 | 21 | --]] 22 | 23 | MAXENTRIES = 5000 24 | 25 | local msg = require 'mp.msg' 26 | local options = require 'mp.options' 27 | local utils = require 'mp.utils' 28 | 29 | o = { 30 | disabled = false, 31 | images = true, 32 | videos = true, 33 | audio = true, 34 | ignore_hidden = true 35 | } 36 | options.read_options(o) 37 | 38 | function Set (t) 39 | local set = {} 40 | for _, v in pairs(t) do set[v] = true end 41 | return set 42 | end 43 | 44 | function SetUnion (a,b) 45 | local res = {} 46 | for k in pairs(a) do res[k] = true end 47 | for k in pairs(b) do res[k] = true end 48 | return res 49 | end 50 | 51 | EXTENSIONS_VIDEO = Set { 52 | '3g2', '3gp', 'avi', 'flv', 'm2ts', 'm4v', 'mj2', 'mkv', 'mov', 53 | 'mp4', 'mpeg', 'mpg', 'ogv', 'rmvb', 'webm', 'wmv', 'y4m' 54 | } 55 | 56 | EXTENSIONS_AUDIO = Set { 57 | 'aiff', 'ape', 'au', 'flac', 'm4a', 'mka', 'mp3', 'oga', 'ogg', 58 | 'ogm', 'opus', 'wav', 'wma' 59 | } 60 | 61 | EXTENSIONS_IMAGES = Set { 62 | 'avif', 'bmp', 'gif', 'j2k', 'jp2', 'jpeg', 'jpg', 'jxl', 'png', 63 | 'svg', 'tga', 'tif', 'tiff', 'webp' 64 | } 65 | 66 | EXTENSIONS = Set {} 67 | if o.videos then EXTENSIONS = SetUnion(EXTENSIONS, EXTENSIONS_VIDEO) end 68 | if o.audio then EXTENSIONS = SetUnion(EXTENSIONS, EXTENSIONS_AUDIO) end 69 | if o.images then EXTENSIONS = SetUnion(EXTENSIONS, EXTENSIONS_IMAGES) end 70 | 71 | function add_files_at(index, files) 72 | index = index - 1 73 | local oldcount = mp.get_property_number("playlist-count", 1) 74 | for i = 1, #files do 75 | mp.commandv("loadfile", files[i], "append") 76 | mp.commandv("playlist-move", oldcount + i - 1, index + i - 1) 77 | end 78 | end 79 | 80 | function get_extension(path) 81 | match = string.match(path, "%.([^%.]+)$" ) 82 | if match == nil then 83 | return "nomatch" 84 | else 85 | return match 86 | end 87 | end 88 | 89 | table.filter = function(t, iter) 90 | for i = #t, 1, -1 do 91 | if not iter(t[i]) then 92 | table.remove(t, i) 93 | end 94 | end 95 | end 96 | 97 | -- alphanum sorting for humans in Lua 98 | -- http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua 99 | 100 | function alphanumsort(filenames) 101 | local function padnum(d) 102 | local dec, n = string.match(d, "(%.?)0*(.+)") 103 | return #dec > 0 and ("%.12f"):format(d) or ("%s%03d%s"):format(dec, #n, n) 104 | end 105 | 106 | local tuples = {} 107 | for i, f in ipairs(filenames) do 108 | tuples[i] = {f:lower():gsub("%.?%d+", padnum), f} 109 | end 110 | table.sort(tuples, function(a, b) 111 | return a[1] == b[1] and #b[2] < #a[2] or a[1] < b[1] 112 | end) 113 | for i, tuple in ipairs(tuples) do filenames[i] = tuple[2] end 114 | return filenames 115 | end 116 | 117 | local autoloaded = nil 118 | 119 | function get_playlist_filenames() 120 | local filenames = {} 121 | for n = 0, pl_count - 1, 1 do 122 | local filename = mp.get_property('playlist/'..n..'/filename') 123 | local _, file = utils.split_path(filename) 124 | filenames[file] = true 125 | end 126 | return filenames 127 | end 128 | 129 | function find_and_add_entries() 130 | local path = mp.get_property("path", "") 131 | local dir, filename = utils.split_path(path) 132 | msg.trace(("dir: %s, filename: %s"):format(dir, filename)) 133 | if o.disabled then 134 | msg.verbose("stopping: autoload disabled") 135 | return 136 | elseif #dir == 0 then 137 | msg.verbose("stopping: not a local path") 138 | return 139 | end 140 | 141 | pl_count = mp.get_property_number("playlist-count", 1) 142 | -- check if this is a manually made playlist 143 | if (pl_count > 1 and autoloaded == nil) or 144 | (pl_count == 1 and EXTENSIONS[string.lower(get_extension(filename))] == nil) then 145 | msg.verbose("stopping: manually made playlist") 146 | return 147 | else 148 | autoloaded = true 149 | end 150 | 151 | local pl = mp.get_property_native("playlist", {}) 152 | local pl_current = mp.get_property_number("playlist-pos-1", 1) 153 | msg.trace(("playlist-pos-1: %s, playlist: %s"):format(pl_current, 154 | utils.to_string(pl))) 155 | 156 | local files = utils.readdir(dir, "files") 157 | if files == nil then 158 | msg.verbose("no other files in directory") 159 | return 160 | end 161 | table.filter(files, function (v, k) 162 | -- The current file could be a hidden file, ignoring it doesn't load other 163 | -- files from the current directory. 164 | if (o.ignore_hidden and not (v == filename) and string.match(v, "^%.")) then 165 | return false 166 | end 167 | local ext = get_extension(v) 168 | if ext == nil then 169 | return false 170 | end 171 | return EXTENSIONS[string.lower(ext)] 172 | end) 173 | alphanumsort(files) 174 | 175 | if dir == "." then 176 | dir = "" 177 | end 178 | 179 | -- Find the current pl entry (dir+"/"+filename) in the sorted dir list 180 | local current 181 | for i = 1, #files do 182 | if files[i] == filename then 183 | current = i 184 | break 185 | end 186 | end 187 | if current == nil then 188 | return 189 | end 190 | msg.trace("current file position in files: "..current) 191 | 192 | local append = {[-1] = {}, [1] = {}} 193 | local filenames = get_playlist_filenames() 194 | for direction = -1, 1, 2 do -- 2 iterations, with direction = -1 and +1 195 | for i = 1, MAXENTRIES do 196 | local file = files[current + i * direction] 197 | if file == nil or file[1] == "." then 198 | break 199 | end 200 | 201 | local filepath = dir .. file 202 | -- skip files already in playlist 203 | if filenames[file] then break end 204 | 205 | if direction == -1 then 206 | if pl_current == 1 then -- never add additional entries in the middle 207 | msg.info("Prepending " .. file) 208 | table.insert(append[-1], 1, filepath) 209 | end 210 | else 211 | msg.info("Adding " .. file) 212 | table.insert(append[1], filepath) 213 | end 214 | end 215 | end 216 | 217 | add_files_at(pl_current + 1, append[1]) 218 | add_files_at(pl_current, append[-1]) 219 | end 220 | 221 | mp.register_event("start-file", find_and_add_entries) 222 | -------------------------------------------------------------------------------- /scripts/playlistmanager.lua: -------------------------------------------------------------------------------- 1 | local settings = { 2 | 3 | -- #### FUNCTIONALITY SETTINGS 4 | 5 | --navigation keybindings force override only while playlist is visible 6 | --if "no" then you can display the playlist by any of the navigation keys 7 | dynamic_binds = true, 8 | 9 | -- to bind multiple keys separate them by a space 10 | key_moveup = "UP", 11 | key_movedown = "DOWN", 12 | key_selectfile = "RIGHT LEFT", 13 | key_unselectfile = "", 14 | key_playfile = "ENTER", 15 | key_removefile = "BS", 16 | key_closeplaylist = "ESC", 17 | 18 | --replaces matches on filenames based on extension, put as empty string to not replace anything 19 | --replace rules are executed in provided order 20 | --replace rule key is the pattern and value is the replace value 21 | --uses :gsub('pattern', 'replace'), read more http://lua-users.org/wiki/StringLibraryTutorial 22 | --'all' will match any extension or protocol if it has one 23 | --uses json and parses it into a lua table to be able to support .conf file 24 | 25 | filename_replace = "", 26 | 27 | --[=====[ START OF SAMPLE REPLACE, to use remove start and end line 28 | --Sample replace: replaces underscore to space on all files 29 | --for mp4 and webm; remove extension, remove brackets and surrounding whitespace, change dot between alphanumeric to space 30 | filename_replace = [[ 31 | [ 32 | { 33 | "ext": { "all": true}, 34 | "rules": [ 35 | { "_" : " " } 36 | ] 37 | },{ 38 | "ext": { "mp4": true, "mkv": true }, 39 | "rules": [ 40 | { "^(.+)%..+$": "%1" }, 41 | { "%s*[%[%(].-[%]%)]%s*": "" }, 42 | { "(%w)%.(%w)": "%1 %2" } 43 | ] 44 | },{ 45 | "protocol": { "http": true, "https": true }, 46 | "rules": [ 47 | { "^%a+://w*%.?": "" } 48 | ] 49 | } 50 | ] 51 | ]], 52 | --END OF SAMPLE REPLACE ]=====] 53 | 54 | --json array of filetypes to search from directory 55 | loadfiles_filetypes = [[ 56 | [ 57 | "jpg", "jpeg", "png", "tif", "tiff", "gif", "webp", "svg", "bmp", 58 | "mp3", "wav", "ogm", "flac", "m4a", "wma", "ogg", "opus", 59 | "mkv", "avi", "mp4", "ogv", "webm", "rmvb", "flv", "wmv", "mpeg", "mpg", "m4v", "3gp" 60 | ] 61 | ]], 62 | 63 | --loadfiles at startup if there is 0 or 1 items in playlist, if 0 uses worḱing dir for files 64 | loadfiles_on_start = false, 65 | 66 | --sort playlist on mpv start 67 | sortplaylist_on_start = false, 68 | 69 | --sort playlist when files are added to playlist 70 | sortplaylist_on_file_add = false, 71 | 72 | --use alphanumerical sort 73 | alphanumsort = true, 74 | 75 | --"linux | windows | auto" 76 | system = "auto", 77 | 78 | --Use ~ for home directory. Leave as empty to use mpv/playlists 79 | playlist_savepath = "", 80 | 81 | --save playlist automatically after current file was unloaded 82 | save_playlist_on_file_end = false, 83 | 84 | 85 | --show playlist or filename every time a new file is loaded 86 | --2 shows playlist, 1 shows current file(filename strip applied) as osd text, 0 shows nothing 87 | --instead of using this you can also call script-message playlistmanager show playlist/filename 88 | --ex. KEY playlist-next ; script-message playlistmanager show playlist 89 | show_playlist_on_fileload = 0, 90 | 91 | --sync cursor when file is loaded from outside reasons(file-ending, playlist-next shortcut etc.) 92 | --has the sideeffect of moving cursor if file happens to change when navigating 93 | --good side is cursor always following current file when going back and forth files with playlist-next/prev 94 | sync_cursor_on_load = true, 95 | 96 | --playlist open key will toggle visibility instead of refresh, best used with long timeout 97 | open_toggles = true, 98 | 99 | --allow the playlist cursor to loop from end to start and vice versa 100 | loop_cursor = true, 101 | 102 | 103 | --#### VISUAL SETTINGS 104 | 105 | --prefer to display titles for following files: "all", "url", "none". Sorting still uses filename. 106 | prefer_titles = "url", 107 | 108 | --call youtube-dl to resolve the titles of urls in the playlist 109 | resolve_titles = false, 110 | 111 | --osd timeout on inactivity, with high value on this open_toggles is good to be true 112 | playlist_display_timeout = 10, 113 | 114 | --amount of entries to show before slicing. Optimal value depends on font/video size etc. 115 | showamount = 16, 116 | 117 | --font size scales by window, if false requires larger font and padding sizes 118 | scale_playlist_by_window=true, 119 | --playlist ass style overrides inside curly brackets, \keyvalue is one field, extra \ for escape in lua 120 | --example {\\fnUbuntu\\fs10\\b0\\bord1} equals: font=Ubuntu, size=10, bold=no, border=1 121 | --read http://docs.aegisub.org/3.2/ASS_Tags/ for reference of tags 122 | --undeclared tags will use default osd settings 123 | --these styles will be used for the whole playlist 124 | style_ass_tags = "{}", 125 | --paddings from top left corner 126 | text_padding_x = 10, 127 | text_padding_y = 30, 128 | 129 | --set title of window with stripped name 130 | set_title_stripped = false, 131 | title_prefix = "", 132 | title_suffix = " - mpv", 133 | 134 | --slice long filenames, and how many chars to show 135 | slice_longfilenames = false, 136 | slice_longfilenames_amount = 70, 137 | 138 | --Playlist header template 139 | --%mediatitle or %filename = title or name of playing file 140 | --%pos = position of playing file 141 | --%cursor = position of navigation 142 | --%plen = playlist length 143 | --%N = newline 144 | playlist_header = "[%cursor/%plen]", 145 | 146 | --Playlist file templates 147 | --%pos = position of file with leading zeros 148 | --%name = title or name of file 149 | --%N = newline 150 | --you can also use the ass tags mentioned above. For example: 151 | -- selected_file="{\\c&HFF00FF&}➔ %name" | to add a color for selected file. However, if you 152 | -- use ass tags you need to reset them for every line (see https://github.com/jonniek/mpv-playlistmanager/issues/20) 153 | normal_file = "○ %name", 154 | hovered_file = "● %name", 155 | selected_file = "➔ %name", 156 | playing_file = "▷ %name", 157 | playing_hovered_file = "▶ %name", 158 | playing_selected_file = "➤ %name", 159 | 160 | 161 | -- what to show when playlist is truncated 162 | playlist_sliced_prefix = "...", 163 | playlist_sliced_suffix = "..." 164 | 165 | } 166 | local opts = require("mp.options") 167 | opts.read_options(settings, "playlistmanager", function(list) update_opts(list) end) 168 | 169 | local utils = require("mp.utils") 170 | local msg = require("mp.msg") 171 | local assdraw = require("mp.assdraw") 172 | 173 | 174 | --check os 175 | if settings.system=="auto" then 176 | local o = {} 177 | if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then 178 | settings.system = "windows" 179 | else 180 | settings.system = "linux" 181 | end 182 | end 183 | 184 | --global variables 185 | local playlist_visible = false 186 | local strippedname = nil 187 | local path = nil 188 | local directory = nil 189 | local filename = nil 190 | local pos = 0 191 | local plen = 0 192 | local cursor = 0 193 | --table for saved media titles for later if we prefer them 194 | local url_table = {} 195 | -- table for urls that we have request to be resolved to titles 196 | local requested_urls = {} 197 | --state for if we sort on playlist size change 198 | local sort_watching = false 199 | 200 | local filetype_lookup = {} 201 | 202 | function update_opts(changelog) 203 | msg.verbose('updating options') 204 | 205 | --parse filename json 206 | if changelog.filename_replace then 207 | if(settings.filename_replace~="") then 208 | settings.filename_replace = utils.parse_json(settings.filename_replace) 209 | else 210 | settings.filename_replace = false 211 | end 212 | end 213 | 214 | --parse loadfiles json 215 | if changelog.loadfiles_filetypes then 216 | settings.loadfiles_filetypes = utils.parse_json(settings.loadfiles_filetypes) 217 | 218 | filetype_lookup = {} 219 | --create loadfiles set 220 | for _, ext in ipairs(settings.loadfiles_filetypes) do 221 | filetype_lookup[ext] = true 222 | end 223 | end 224 | 225 | if changelog.resolve_titles then 226 | resolve_titles() 227 | end 228 | 229 | if changelog.playlist_display_timeout then 230 | keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds) 231 | keybindstimer:kill() 232 | end 233 | 234 | if playlist_visible then showplaylist() end 235 | end 236 | 237 | update_opts({filename_replace = true, loadfiles_filetypes = true}) 238 | 239 | function on_loaded() 240 | filename = mp.get_property("filename") 241 | path = mp.get_property('path') 242 | --if not a url then join path with working directory 243 | if not path:match("^%a%a+:%/%/") then 244 | path = utils.join_path(mp.get_property('working-directory'), path) 245 | directory = utils.split_path(path) 246 | else 247 | directory = nil 248 | end 249 | 250 | refresh_globals() 251 | if settings.sync_cursor_on_load then 252 | cursor=pos 253 | --refresh playlist if cursor moved 254 | if playlist_visible then draw_playlist() end 255 | end 256 | 257 | local media_title = mp.get_property("media-title") 258 | if path:match('^https?://') and not url_table[path] and path ~= media_title then 259 | url_table[path] = media_title 260 | end 261 | 262 | strippedname = stripfilename(mp.get_property('media-title')) 263 | if settings.show_playlist_on_fileload == 2 then 264 | showplaylist() 265 | elseif settings.show_playlist_on_fileload == 1 then 266 | mp.commandv('show-text', strippedname) 267 | end 268 | if settings.set_title_stripped then 269 | mp.set_property("title", settings.title_prefix..strippedname..settings.title_suffix) 270 | end 271 | 272 | local didload = false 273 | if settings.loadfiles_on_start and plen == 1 then 274 | didload = true --save reference for sorting 275 | msg.info("Loading files from playing files directory") 276 | playlist() 277 | end 278 | 279 | --if we promised to sort files on launch do it 280 | if promised_sort then 281 | promised_sort = false 282 | msg.info("Your playlist is sorted before starting playback") 283 | if didload then sortplaylist() else sortplaylist(true) end 284 | end 285 | 286 | --if we promised to listen and sort on playlist size increase do it 287 | if promised_sort_watch then 288 | promised_sort_watch = false 289 | sort_watching = true 290 | msg.info("Added files will be automatically sorted") 291 | mp.observe_property('playlist-count', "number", autosort) 292 | end 293 | end 294 | 295 | function on_closed() 296 | if settings.save_playlist_on_file_end then save_playlist() end 297 | strippedname = nil 298 | path = nil 299 | directory = nil 300 | filename = nil 301 | if playlist_visible then showplaylist() end 302 | end 303 | 304 | function refresh_globals() 305 | pos = mp.get_property_number('playlist-pos', 0) 306 | plen = mp.get_property_number('playlist-count', 0) 307 | end 308 | 309 | function escapepath(dir, escapechar) 310 | return string.gsub(dir, escapechar, '\\'..escapechar) 311 | end 312 | 313 | --strip a filename based on its extension or protocol according to rules in settings 314 | function stripfilename(pathfile, media_title) 315 | if pathfile == nil then return '' end 316 | local ext = pathfile:match("^.+%.(.+)$") 317 | local protocol = pathfile:match("^(%a%a+)://") 318 | if not ext then ext = "" end 319 | local tmp = pathfile 320 | if settings.filename_replace and not media_title then 321 | for k,v in ipairs(settings.filename_replace) do 322 | if ( v['ext'] and (v['ext'][ext] or (ext and not protocol and v['ext']['all'])) ) 323 | or ( v['protocol'] and (v['protocol'][protocol] or (protocol and not ext and v['protocol']['all'])) ) then 324 | for ruleindex, indexrules in ipairs(v['rules']) do 325 | for rule, override in pairs(indexrules) do 326 | tmp = tmp:gsub(rule, override) 327 | end 328 | end 329 | end 330 | end 331 | end 332 | if settings.slice_longfilenames and tmp:len()>settings.slice_longfilenames_amount+5 then 333 | tmp = tmp:sub(1, settings.slice_longfilenames_amount).." ..." 334 | end 335 | return tmp 336 | end 337 | 338 | --gets a nicename of playlist entry at 0-based position i 339 | function get_name_from_index(i, notitle) 340 | refresh_globals() 341 | if plen <= i then msg.error("no index in playlist", i, "length", plen); return nil end 342 | local _, name = nil 343 | local title = mp.get_property('playlist/'..i..'/title') 344 | local name = mp.get_property('playlist/'..i..'/filename') 345 | 346 | local should_use_title = settings.prefer_titles == 'all' or name:match('^https?://') and settings.prefer_titles == 'url' 347 | --check if file has a media title stored or as property 348 | if not title and should_use_title then 349 | local mtitle = mp.get_property('media-title') 350 | if i == pos and mp.get_property('filename') ~= mtitle then 351 | if not url_table[name] then 352 | url_table[name] = mtitle 353 | end 354 | title = mtitle 355 | elseif url_table[name] then 356 | title = url_table[name] 357 | end 358 | end 359 | 360 | --if we have media title use a more conservative strip 361 | if title and not notitle and should_use_title then return stripfilename(title, true) end 362 | 363 | --remove paths if they exist, keeping protocols for stripping 364 | if string.sub(name, 1, 1) == '/' or name:match("^%a:[/\\]") then 365 | _, name = utils.split_path(name) 366 | end 367 | return stripfilename(name) 368 | end 369 | 370 | function parse_header(string) 371 | local esc_title = stripfilename(mp.get_property("media-title"), true):gsub("%%", "%%%%") 372 | local esc_file = stripfilename(mp.get_property("filename")):gsub("%%", "%%%%") 373 | return string:gsub("%%N", "\\N") 374 | :gsub("%%pos", mp.get_property_number("playlist-pos",0)+1) 375 | :gsub("%%plen", mp.get_property("playlist-count")) 376 | :gsub("%%cursor", cursor+1) 377 | :gsub("%%mediatitle", esc_title) 378 | :gsub("%%filename", esc_file) 379 | -- undo name escape 380 | :gsub("%%%%", "%%") 381 | end 382 | 383 | function parse_filename(string, name, index) 384 | local base = tostring(plen):len() 385 | local esc_name = stripfilename(name):gsub("%%", "%%%%") 386 | return string:gsub("%%N", "\\N") 387 | :gsub("%%pos", string.format("%0"..base.."d", index+1)) 388 | :gsub("%%name", esc_name) 389 | -- undo name escape 390 | :gsub("%%%%", "%%") 391 | end 392 | 393 | function parse_filename_by_index(index) 394 | local template = settings.normal_file 395 | 396 | local is_idle = mp.get_property_native('idle-active') 397 | local position = is_idle and -1 or pos 398 | 399 | if index == position then 400 | if index == cursor then 401 | if selection then 402 | template = settings.playing_selected_file 403 | else 404 | template = settings.playing_hovered_file 405 | end 406 | else 407 | template = settings.playing_file 408 | end 409 | elseif index == cursor then 410 | if selection then 411 | template = settings.selected_file 412 | else 413 | template = settings.hovered_file 414 | end 415 | end 416 | 417 | return parse_filename(template, get_name_from_index(index), index) 418 | end 419 | 420 | 421 | function draw_playlist() 422 | refresh_globals() 423 | local ass = assdraw.ass_new() 424 | ass:new_event() 425 | ass:pos(settings.text_padding_x, settings.text_padding_y) 426 | ass:append(settings.style_ass_tags) 427 | 428 | if settings.playlist_header ~= "" then 429 | ass:append(parse_header(settings.playlist_header).."\\N") 430 | end 431 | local start = cursor - math.floor(settings.showamount/2) 432 | local showall = false 433 | local showrest = false 434 | if start<0 then start=0 end 435 | if plen <= settings.showamount then 436 | start=0 437 | showall=true 438 | end 439 | if start > math.max(plen-settings.showamount-1, 0) then 440 | start=plen-settings.showamount 441 | showrest=true 442 | end 443 | if start > 0 and not showall then ass:append(settings.playlist_sliced_prefix.."\\N") end 444 | for index=start,start+settings.showamount-1,1 do 445 | if index == plen then break end 446 | 447 | ass:append(parse_filename_by_index(index).."\\N") 448 | if index == start+settings.showamount-1 and not showall and not showrest then 449 | ass:append(settings.playlist_sliced_suffix) 450 | end 451 | end 452 | local w, h = mp.get_osd_size() 453 | if settings.scale_playlist_by_window then w,h = 0, 0 end 454 | mp.set_osd_ass(w, h, ass.text) 455 | end 456 | 457 | function toggle_playlist() 458 | if settings.open_toggles then 459 | if playlist_visible then 460 | remove_keybinds() 461 | return 462 | end 463 | end 464 | showplaylist() 465 | end 466 | 467 | function showplaylist(duration) 468 | refresh_globals() 469 | if plen == 0 then return end 470 | playlist_visible = true 471 | add_keybinds() 472 | 473 | draw_playlist() 474 | keybindstimer:kill() 475 | if duration then 476 | keybindstimer = mp.add_periodic_timer(duration, remove_keybinds) 477 | else 478 | keybindstimer:resume() 479 | end 480 | end 481 | 482 | selection=nil 483 | function selectfile() 484 | refresh_globals() 485 | if plen == 0 then return end 486 | if not selection then 487 | selection=cursor 488 | else 489 | selection=nil 490 | end 491 | showplaylist() 492 | end 493 | 494 | function unselectfile() 495 | selection=nil 496 | showplaylist() 497 | end 498 | 499 | function removefile() 500 | refresh_globals() 501 | if plen == 0 then return end 502 | selection = nil 503 | if cursor==pos then mp.command("script-message unseenplaylist mark true \"playlistmanager avoid conflict when removing file\"") end 504 | mp.commandv("playlist-remove", cursor) 505 | if cursor==plen-1 then cursor = cursor - 1 end 506 | showplaylist() 507 | end 508 | 509 | function moveup() 510 | refresh_globals() 511 | if plen == 0 then return end 512 | if cursor~=0 then 513 | if selection then mp.commandv("playlist-move", cursor,cursor-1) end 514 | cursor = cursor-1 515 | elseif settings.loop_cursor then 516 | if selection then mp.commandv("playlist-move", cursor,plen) end 517 | cursor = plen-1 518 | end 519 | showplaylist() 520 | end 521 | 522 | function movedown() 523 | refresh_globals() 524 | if plen == 0 then return end 525 | if cursor ~= plen-1 then 526 | if selection then mp.commandv("playlist-move", cursor,cursor+2) end 527 | cursor = cursor + 1 528 | elseif settings.loop_cursor then 529 | if selection then mp.commandv("playlist-move", cursor,0) end 530 | cursor = 0 531 | end 532 | showplaylist() 533 | end 534 | 535 | function write_watch_later(force_write) 536 | if mp.get_property_bool("save-position-on-quit") or force_write then 537 | mp.command("write-watch-later-config") 538 | end 539 | end 540 | 541 | function playlist_next(force_write) 542 | write_watch_later(force_write) 543 | mp.commandv("playlist-next", "weak") 544 | end 545 | 546 | function playlist_prev(force_write) 547 | write_watch_later(force_write) 548 | mp.commandv("playlist-prev", "weak") 549 | end 550 | 551 | function playfile() 552 | refresh_globals() 553 | if plen == 0 then return end 554 | selection = nil 555 | local is_idle = mp.get_property_native('idle-active') 556 | if cursor ~= pos or is_idle then 557 | write_watch_later() 558 | mp.set_property("playlist-pos", cursor) 559 | else 560 | if cursor~=plen-1 then 561 | cursor = cursor + 1 562 | end 563 | write_watch_later() 564 | mp.commandv("playlist-next", "weak") 565 | end 566 | if settings.show_playlist_on_fileload ~= 2 then 567 | remove_keybinds() 568 | end 569 | end 570 | 571 | function get_files_windows(dir) 572 | local args = { 573 | 'powershell', '-NoProfile', '-Command', [[& { 574 | Trap { 575 | Write-Error -ErrorRecord $_ 576 | Exit 1 577 | } 578 | $path = "]]..dir..[[" 579 | $escapedPath = [WildcardPattern]::Escape($path) 580 | cd $escapedPath 581 | 582 | $list = (Get-ChildItem -File | Sort-Object { [regex]::Replace($_.Name, '\d+', { $args[0].Value.PadLeft(20) }) }).Name 583 | $string = ($list -join "/") 584 | $u8list = [System.Text.Encoding]::UTF8.GetBytes($string) 585 | [Console]::OpenStandardOutput().Write($u8list, 0, $u8list.Length) 586 | }]] 587 | } 588 | local process = utils.subprocess({ args = args, cancellable = false }) 589 | return parse_files(process, '%/') 590 | end 591 | 592 | function get_files_linux(dir) 593 | local args = { 'ls', '-1pv', dir } 594 | local process = utils.subprocess({ args = args, cancellable = false }) 595 | return parse_files(process, '\n') 596 | end 597 | 598 | function parse_files(res, delimiter) 599 | if not res.error and res.status == 0 then 600 | local valid_files = {} 601 | for line in res.stdout:gmatch("[^"..delimiter.."]+") do 602 | local ext = line:match("^.+%.(.+)$") 603 | if ext and filetype_lookup[ext:lower()] then 604 | table.insert(valid_files, line) 605 | end 606 | end 607 | return valid_files, nil 608 | else 609 | return nil, res.error 610 | end 611 | end 612 | 613 | --Creates a playlist of all files in directory, will keep the order and position 614 | --For exaple, Folder has 12 files, you open the 5th file and run this, the remaining 7 are added behind the 5th file and prior 4 files before it 615 | function playlist(force_dir) 616 | refresh_globals() 617 | if not directory and plen > 0 then return end 618 | local hasfile = true 619 | if plen == 0 then 620 | hasfile = false 621 | dir = mp.get_property('working-directory') 622 | else 623 | dir = directory 624 | end 625 | if force_dir then dir = force_dir end 626 | 627 | local files, error 628 | if settings.system == "linux" then 629 | files, error = get_files_linux(dir) 630 | else 631 | files, error = get_files_windows(dir) 632 | end 633 | 634 | local c, c2 = 0,0 635 | if files then 636 | local cur = false 637 | local filename = mp.get_property("filename") 638 | for _, file in ipairs(files) do 639 | local appendstr = "append" 640 | if not hasfile then 641 | cur = true 642 | appendstr = "append-play" 643 | hasfile = true 644 | end 645 | if cur == true then 646 | mp.commandv("loadfile", utils.join_path(dir, file), appendstr) 647 | msg.info("Appended to playlist: " .. file) 648 | c2 = c2 + 1 649 | elseif file ~= filename then 650 | mp.commandv("loadfile", utils.join_path(dir, file), appendstr) 651 | msg.info("Prepended to playlist: " .. file) 652 | mp.commandv("playlist-move", mp.get_property_number("playlist-count", 1)-1, c) 653 | c = c + 1 654 | else 655 | cur = true 656 | end 657 | end 658 | if c2 > 0 or c>0 then 659 | mp.osd_message("Added "..c + c2.." files to playlist") 660 | else 661 | mp.osd_message("No additional files found") 662 | end 663 | cursor = mp.get_property_number('playlist-pos', 1) 664 | else 665 | msg.error("Could not scan for files: "..(error or "")) 666 | end 667 | if sort_watching then 668 | msg.info("Ignoring directory structure and using playlist sort") 669 | sortplaylist() 670 | end 671 | refresh_globals() 672 | if playlist_visible then showplaylist() end 673 | return c + c2 674 | end 675 | 676 | function parse_home(path) 677 | if not path:find("^~") then 678 | return path 679 | end 680 | local home_dir = os.getenv("HOME") or os.getenv("USERPROFILE") 681 | if not home_dir then 682 | local drive = os.getenv("HOMEDRIVE") 683 | local path = os.getenv("HOMEPATH") 684 | if drive and path then 685 | home_dir = utils.join_path(drive, path) 686 | else 687 | msg.error("Couldn't find home dir.") 688 | return nil 689 | end 690 | end 691 | local result = path:gsub("^~", home_dir) 692 | return result 693 | end 694 | 695 | --saves the current playlist into a m3u file 696 | function save_playlist() 697 | local length = mp.get_property_number('playlist-count', 0) 698 | if length == 0 then return end 699 | 700 | --get playlist save path 701 | local savepath 702 | if settings.playlist_savepath == nil or settings.playlist_savepath == "" then 703 | savepath = mp.command_native({"expand-path", "~~home/"}).."/playlists" 704 | else 705 | savepath = parse_home(settings.playlist_savepath) 706 | if savepath == nil then return end 707 | end 708 | 709 | --create savepath if it doesn't exist 710 | if utils.readdir(savepath) == nil then 711 | local windows_args = {'powershell', '-NoProfile', '-Command', 'mkdir', savepath} 712 | local unix_args = { 'mkdir', savepath } 713 | local args = settings.system == 'windows' and windows_args or unix_args 714 | local res = utils.subprocess({ args = args, cancellable = false }) 715 | if res.status ~= 0 then 716 | msg.error("Failed to create playlist save directory "..savepath..". Error: "..(res.error or "unknown")) 717 | return 718 | end 719 | end 720 | 721 | local date = os.date("*t") 722 | local datestring = ("%02d-%02d-%02d_%02d-%02d-%02d"):format(date.year, date.month, date.day, date.hour, date.min, date.sec) 723 | 724 | local savepath = utils.join_path(savepath, datestring.."_playlist-size_"..length..".m3u") 725 | local file, err = io.open(savepath, "w") 726 | if not file then 727 | msg.error("Error in creating playlist file, check permissions. Error: "..(err or "unknown")) 728 | else 729 | local i=0 730 | while i < length do 731 | local pwd = mp.get_property("working-directory") 732 | local filename = mp.get_property('playlist/'..i..'/filename') 733 | local fullpath = filename 734 | if not filename:match("^%a%a+:%/%/") then 735 | fullpath = utils.join_path(pwd, filename) 736 | end 737 | local title = mp.get_property('playlist/'..i..'/title') 738 | if title then file:write("#EXTINF:,"..title.."\n") end 739 | file:write(fullpath, "\n") 740 | i=i+1 741 | end 742 | msg.info("Playlist written to: "..savepath) 743 | file:close() 744 | end 745 | end 746 | 747 | function alphanumsort(a, b) 748 | local function padnum(d) 749 | local dec, n = string.match(d, "(%.?)0*(.+)") 750 | return #dec > 0 and ("%.12f"):format(d) or ("%s%03d%s"):format(dec, #n, n) 751 | end 752 | return tostring(a):lower():gsub("%.?%d+",padnum)..("%3d"):format(#b) 753 | < tostring(b):lower():gsub("%.?%d+",padnum)..("%3d"):format(#a) 754 | end 755 | 756 | function dosort(a,b) 757 | if settings.alphanumsort then 758 | return alphanumsort(a,b) 759 | else 760 | return a < b 761 | end 762 | end 763 | 764 | function sortplaylist(startover) 765 | local length = mp.get_property_number('playlist-count', 0) 766 | if length < 2 then return end 767 | --use insertion sort on playlist to make it easy to order files with playlist-move 768 | for outer=1, length-1, 1 do 769 | local outerfile = get_name_from_index(outer, true) 770 | local inner = outer - 1 771 | while inner >= 0 and dosort(outerfile, get_name_from_index(inner, true)) do 772 | inner = inner - 1 773 | end 774 | inner = inner + 1 775 | if outer ~= inner then 776 | mp.commandv('playlist-move', outer, inner) 777 | end 778 | end 779 | cursor = mp.get_property_number('playlist-pos', 0) 780 | if startover then 781 | mp.set_property('playlist-pos', 0) 782 | end 783 | if playlist_visible then showplaylist() end 784 | end 785 | 786 | function autosort(name, param) 787 | if param == 0 then return end 788 | if plen < param then 789 | msg.info("Playlistmanager autosorting playlist") 790 | refresh_globals() 791 | sortplaylist() 792 | end 793 | end 794 | 795 | function reverseplaylist() 796 | local length = mp.get_property_number('playlist-count', 0) 797 | if length < 2 then return end 798 | for outer=1, length-1, 1 do 799 | mp.commandv('playlist-move', outer, 0) 800 | end 801 | if playlist_visible then showplaylist() end 802 | end 803 | 804 | function shuffleplaylist() 805 | refresh_globals() 806 | if plen < 2 then return end 807 | mp.command("playlist-shuffle") 808 | math.randomseed(os.time()) 809 | mp.commandv("playlist-move", pos, math.random(0, plen-1)) 810 | mp.set_property('playlist-pos', 0) 811 | refresh_globals() 812 | if playlist_visible then showplaylist() end 813 | end 814 | 815 | function bind_keys(keys, name, func, opts) 816 | if not keys then 817 | mp.add_forced_key_binding(keys, name, func, opts) 818 | return 819 | end 820 | local i = 1 821 | for key in keys:gmatch("[^%s]+") do 822 | local prefix = i == 1 and '' or i 823 | mp.add_forced_key_binding(key, name..prefix, func, opts) 824 | i = i + 1 825 | end 826 | end 827 | 828 | function unbind_keys(keys, name) 829 | if not keys then 830 | mp.remove_key_binding(name) 831 | return 832 | end 833 | local i = 1 834 | for key in keys:gmatch("[^%s]+") do 835 | local prefix = i == 1 and '' or i 836 | mp.remove_key_binding(name..prefix) 837 | i = i + 1 838 | end 839 | end 840 | 841 | function add_keybinds() 842 | bind_keys(settings.key_moveup, 'moveup', moveup, "repeatable") 843 | bind_keys(settings.key_movedown, 'movedown', movedown, "repeatable") 844 | bind_keys(settings.key_selectfile, 'selectfile', selectfile) 845 | bind_keys(settings.key_unselectfile, 'unselectfile', unselectfile) 846 | bind_keys(settings.key_playfile, 'playfile', playfile) 847 | bind_keys(settings.key_removefile, 'removefile', removefile, "repeatable") 848 | bind_keys(settings.key_closeplaylist, 'closeplaylist', remove_keybinds) 849 | end 850 | 851 | function remove_keybinds() 852 | keybindstimer:kill() 853 | keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds) 854 | keybindstimer:kill() 855 | mp.set_osd_ass(0, 0, "") 856 | playlist_visible = false 857 | if settings.dynamic_binds then 858 | unbind_keys(settings.key_moveup, 'moveup') 859 | unbind_keys(settings.key_movedown, 'movedown') 860 | unbind_keys(settings.key_selectfile, 'selectfile') 861 | unbind_keys(settings.key_unselectfile, 'unselectfile') 862 | unbind_keys(settings.key_playfile, 'playfile') 863 | unbind_keys(settings.key_removefile, 'removefile') 864 | unbind_keys(settings.key_closeplaylist, 'closeplaylist') 865 | end 866 | end 867 | 868 | keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds) 869 | keybindstimer:kill() 870 | 871 | if not settings.dynamic_binds then 872 | add_keybinds() 873 | end 874 | 875 | if settings.loadfiles_on_start and mp.get_property_number('playlist-count', 0) == 0 then 876 | playlist() 877 | end 878 | 879 | promised_sort_watch = false 880 | if settings.sortplaylist_on_file_add then 881 | promised_sort_watch = true 882 | end 883 | 884 | promised_sort = false 885 | if settings.sortplaylist_on_start then 886 | promised_sort = true 887 | end 888 | 889 | mp.observe_property('playlist-count', "number", function() 890 | if playlist_visible then showplaylist() end 891 | if settings.prefer_titles == 'none' then return end 892 | -- resolve titles 893 | resolve_titles() 894 | end) 895 | 896 | --resolves url titles by calling youtube-dl 897 | function resolve_titles() 898 | if not settings.resolve_titles then return end 899 | local length = mp.get_property_number('playlist-count', 0) 900 | if length < 2 then return end 901 | local i=0 902 | -- loop all items in playlist because we can't predict how it has changed 903 | while i < length do 904 | local filename = mp.get_property('playlist/'..i..'/filename') 905 | local title = mp.get_property('playlist/'..i..'/title') 906 | if i ~= pos 907 | and filename 908 | and filename:match('^https?://') 909 | and not title 910 | and not url_table[filename] 911 | and not requested_urls[filename] 912 | then 913 | requested_urls[filename] = true 914 | 915 | local args = { 'youtube-dl', '--no-playlist', '--flat-playlist', '-sJ', filename } 916 | local req = mp.command_native_async( 917 | { 918 | name = "subprocess", 919 | args = args, 920 | playback_only = false, 921 | capture_stdout = true 922 | }, function (success, res) 923 | if res.killed_by_us then 924 | msg.verbose('Request to resolve url title ' .. filename .. ' timed out') 925 | return 926 | end 927 | if res.status == 0 then 928 | local json, err = utils.parse_json(res.stdout) 929 | if not err then 930 | local is_playlist = json['_type'] and json['_type'] == 'playlist' 931 | local title = (is_playlist and '[playlist]: ' or '') .. json['title'] 932 | msg.verbose(filename .. " resolved to '" .. title .. "'") 933 | url_table[filename] = title 934 | refresh_globals() 935 | if playlist_visible then showplaylist() end 936 | return 937 | else 938 | msg.error("Failed parsing json, reason: "..(err or "unknown")) 939 | end 940 | else 941 | msg.error("Failed to resolve url title "..filename.." Error: "..(res.error or "unknown")) 942 | end 943 | end) 944 | 945 | mp.add_timeout(5, function() 946 | mp.abort_async_command(req) 947 | end) 948 | 949 | end 950 | i=i+1 951 | end 952 | end 953 | 954 | --script message handler 955 | function handlemessage(msg, value, value2) 956 | if msg == "show" and value == "playlist" then 957 | if value2 ~= "toggle" then 958 | showplaylist(value2) 959 | return 960 | else 961 | toggle_playlist() 962 | return 963 | end 964 | end 965 | if msg == "show" and value == "filename" and strippedname and value2 then 966 | mp.commandv('show-text', strippedname, tonumber(value2)*1000 ) ; return 967 | end 968 | if msg == "show" and value == "filename" and strippedname then 969 | mp.commandv('show-text', strippedname ) ; return 970 | end 971 | if msg == "sort" then sortplaylist(value) ; return end 972 | if msg == "shuffle" then shuffleplaylist() ; return end 973 | if msg == "reverse" then reverseplaylist() ; return end 974 | if msg == "loadfiles" then playlist(value) ; return end 975 | if msg == "save" then save_playlist() ; return end 976 | if msg == "playlist-next" then playlist_next(true) ; return end 977 | if msg == "playlist-prev" then playlist_prev(true) ; return end 978 | end 979 | 980 | mp.register_script_message("playlistmanager", handlemessage) 981 | 982 | mp.add_key_binding("CTRL+p", "sortplaylist", sortplaylist) 983 | mp.add_key_binding("CTRL+P", "shuffleplaylist", shuffleplaylist) 984 | mp.add_key_binding("CTRL+R", "reverseplaylist", reverseplaylist) 985 | mp.add_key_binding("P", "loadfiles", playlist) 986 | mp.add_key_binding("p", "saveplaylist", save_playlist) 987 | mp.add_key_binding("SHIFT+ENTER", "showplaylist", toggle_playlist) 988 | 989 | mp.register_event("file-loaded", on_loaded) 990 | mp.register_event("end-file", on_closed) 991 | -------------------------------------------------------------------------------- /scripts/quality-menu.lua: -------------------------------------------------------------------------------- 1 | -- quality-menu 3.0.1 - 2022-Dec-11 2 | -- https://github.com/christoph-heinrich/mpv-quality-menu 3 | -- 4 | -- Change the stream video and audio quality on the fly. 5 | -- 6 | -- Usage: 7 | -- add bindings to input.conf: 8 | -- F script-binding quality_menu/video_formats_toggle 9 | -- Alt+f script-binding quality_menu/audio_formats_toggle 10 | 11 | local mp = require 'mp' 12 | local utils = require 'mp.utils' 13 | local msg = require 'mp.msg' 14 | local assdraw = require 'mp.assdraw' 15 | local opt = require('mp.options') 16 | 17 | local opts = { 18 | --key bindings 19 | up_binding = "UP WHEEL_UP", 20 | down_binding = "DOWN WHEEL_DOWN", 21 | select_binding = "ENTER MBTN_LEFT", 22 | close_menu_binding = "ESC MBTN_RIGHT F Alt+f", 23 | 24 | --youtube-dl version(could be youtube-dl or yt-dlp, or something else) 25 | ytdl_ver = "yt-dlp", 26 | 27 | --formatting / cursors 28 | selected_and_active = "▶ - ", 29 | selected_and_inactive = "● - ", 30 | unselected_and_active = "▷ - ", 31 | unselected_and_inactive = "○ - ", 32 | 33 | --font size scales by window, if false requires larger font and padding sizes 34 | scale_playlist_by_window = true, 35 | 36 | --playlist ass style overrides inside curly brackets, \keyvalue is one field, extra \ for escape in lua 37 | --example {\\fnUbuntu\\fs10\\b0\\bord1} equals: font=Ubuntu, size=10, bold=no, border=1 38 | --read http://docs.aegisub.org/3.2/ASS_Tags/ for reference of tags 39 | --undeclared tags will use default osd settings 40 | --these styles will be used for the whole playlist. More specific styling will need to be hacked in 41 | -- 42 | --(a monospaced font is recommended but not required) 43 | style_ass_tags = "{\\fnmonospace\\fs25\\bord1}", 44 | 45 | -- Shift drawing coordinates. Required for mpv.net compatiblity 46 | shift_x = 0, 47 | shift_y = 0, 48 | 49 | --paddings from window edge 50 | text_padding_x = 5, 51 | text_padding_y = 10, 52 | 53 | --Screen dim when menu is open 54 | curtain_opacity = 0.7, 55 | 56 | --how many seconds until the quality menu times out 57 | --setting this to 0 deactivates the timeout 58 | menu_timeout = 6, 59 | 60 | --use youtube-dl to fetch a list of available formats (overrides quality_strings) 61 | fetch_formats = true, 62 | 63 | --default menu entries 64 | quality_strings = [[ 65 | [ 66 | {"4320p" : "bestvideo[height<=?4320p]+bestaudio/best"}, 67 | {"2160p" : "bestvideo[height<=?2160]+bestaudio/best"}, 68 | {"1440p" : "bestvideo[height<=?1440]+bestaudio/best"}, 69 | {"1080p" : "bestvideo[height<=?1080]+bestaudio/best"}, 70 | {"720p" : "bestvideo[height<=?720]+bestaudio/best"}, 71 | {"480p" : "bestvideo[height<=?480]+bestaudio/best"}, 72 | {"360p" : "bestvideo[height<=?360]+bestaudio/best"}, 73 | {"240p" : "bestvideo[height<=?240]+bestaudio/best"}, 74 | {"144p" : "bestvideo[height<=?144]+bestaudio/best"} 75 | ] 76 | ]], 77 | 78 | --reset ytdl-format to the original format string when changing files (e.g. going to the next playlist entry) 79 | --if file was opened previously, reset to previously selected format 80 | reset_format = true, 81 | 82 | --automatically fetch available formats when opening an url 83 | fetch_on_start = true, 84 | 85 | --show the video format menu after opening an url 86 | start_with_menu = false, 87 | 88 | --include unknown formats in the list 89 | --Unfortunately choosing which formats are video or audio is not always perfect. 90 | --Set to true to make sure you don't miss any formats, but then the list 91 | --might also include formats that aren't actually video or audio. 92 | --Formats that are known to not be video or audio are still filtered out. 93 | include_unknown = false, 94 | 95 | --hide columns that are identical for all formats 96 | hide_identical_columns = true, 97 | 98 | --which columns are shown in which order 99 | --comma separated list, prefix column with "-" to align left 100 | -- 101 | --columns that might be useful are: 102 | --resolution, width, height, fps, dynamic_range, tbr, vbr, abr, asr, 103 | --filesize, filesize_approx, vcodec, acodec, ext, video_ext, audio_ext, 104 | --language, format, format_note, quality 105 | -- 106 | --columns that are derived from the above, but with special treatment: 107 | --frame_rate, bitrate_total, bitrate_video, bitrate_audio, 108 | --codec_video, codec_audio, audio_sample_rate 109 | -- 110 | --If those still aren't enough or you're just curious, run: 111 | --yt-dlp -j 112 | --This outputs unformatted JSON. 113 | --Format it and look under "formats" to see what's available. 114 | -- 115 | --Not all videos have all columns available. 116 | --Be careful, misspelled columns simply won't be displayed, there is no error. 117 | columns_video = '-resolution,frame_rate,dynamic_range,language,bitrate_total,size,-codec_video,-codec_audio', 118 | columns_audio = 'audio_sample_rate,bitrate_total,size,language,-codec_audio', 119 | 120 | --columns used for sorting, see "columns_video" for available columns 121 | --comma separated list, prefix column with "-" to reverse sorting order 122 | --Leaving this empty keeps the order from yt-dlp/youtube-dl. 123 | --Be careful, misspelled columns won't result in an error, 124 | --but they might influence the result. 125 | sort_video = 'height,fps,tbr,size,format_id', 126 | sort_audio = 'asr,tbr,size,format_id', 127 | } 128 | opt.read_options(opts, "quality-menu") 129 | opts.quality_strings = utils.parse_json(opts.quality_strings) 130 | 131 | opts.font_size = tonumber(opts.style_ass_tags:match('\\fs(%d+%.?%d*)')) or mp.get_property_number('osd-font-size') or 25 132 | opts.curtain_opacity = math.max(math.min(opts.curtain_opacity, 1), 0) 133 | 134 | -- special thanks to reload.lua (https://github.com/4e6/mpv-reload/) 135 | local function reload_resume() 136 | local playlist_pos = mp.get_property_number("playlist-pos") 137 | local reload_duration = mp.get_property_native("duration") 138 | local time_pos = mp.get_property("time-pos") 139 | 140 | mp.set_property_number("playlist-pos", playlist_pos) 141 | 142 | -- Tries to determine live stream vs. pre-recorded VOD. VOD has non-zero 143 | -- duration property. When reloading VOD, to keep the current time position 144 | -- we should provide offset from the start. Stream doesn't have fixed start. 145 | -- Decent choice would be to reload stream from it's current 'live' position. 146 | -- That's the reason we don't pass the offset when reloading streams. 147 | if reload_duration and reload_duration > 0 then 148 | local function seeker() 149 | mp.commandv("seek", time_pos, "absolute") 150 | mp.unregister_event(seeker) 151 | end 152 | 153 | mp.register_event("file-loaded", seeker) 154 | end 155 | end 156 | 157 | local ytdl = { 158 | path = opts.ytdl_ver, 159 | searched = false, 160 | blacklisted = {} 161 | } 162 | 163 | local function process_json(json) 164 | local function is_video(format) 165 | -- "none" means it is not a video 166 | -- nil means it is unknown 167 | return (opts.include_unknown or format.vcodec) and format.vcodec ~= "none" 168 | end 169 | 170 | local function is_audio(format) 171 | return (opts.include_unknown or format.acodec) and format.acodec ~= "none" 172 | end 173 | 174 | local vfmt = nil 175 | local afmt = nil 176 | local requested_formats = json["requested_formats"] or json["requested_downloads"] 177 | for _, format in ipairs(requested_formats) do 178 | if is_video(format) then 179 | vfmt = format["format_id"] 180 | elseif is_audio(format) then 181 | afmt = format["format_id"] 182 | end 183 | end 184 | 185 | local video_formats = {} 186 | local audio_formats = {} 187 | local all_formats = {} 188 | for i = #json.formats, 1, -1 do 189 | local format = json.formats[i] 190 | if is_video(format) then 191 | video_formats[#video_formats + 1] = format 192 | all_formats[#all_formats + 1] = format 193 | elseif is_audio(format) then 194 | audio_formats[#audio_formats + 1] = format 195 | all_formats[#all_formats + 1] = format 196 | end 197 | end 198 | 199 | local function populate_special_fields(format) 200 | format.size = format.filesize or format.filesize_approx 201 | format.frame_rate = format.fps 202 | format.bitrate_total = format.tbr 203 | format.bitrate_video = format.vbr 204 | format.bitrate_audio = format.abr 205 | format.codec_video = format.vcodec 206 | format.codec_audio = format.acodec 207 | format.audio_sample_rate = format.asr 208 | end 209 | 210 | for _, format in ipairs(all_formats) do 211 | populate_special_fields(format) 212 | end 213 | 214 | local function strip_minus(list) 215 | local stripped_list = {} 216 | local had_minus = {} 217 | for i, val in ipairs(list) do 218 | if string.sub(val, 1, 1) == "-" then 219 | val = string.sub(val, 2) 220 | had_minus[val] = true 221 | end 222 | stripped_list[i] = val 223 | end 224 | return stripped_list, had_minus 225 | end 226 | 227 | local function string_split(inputstr, sep) 228 | if sep == nil then 229 | sep = "%s" 230 | end 231 | local t = {} 232 | for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do 233 | table.insert(t, str) 234 | end 235 | return t 236 | end 237 | 238 | local sort_video, reverse_video = strip_minus(string_split(opts.sort_video, ',')) 239 | local sort_audio, reverse_audio = strip_minus(string_split(opts.sort_audio, ',')) 240 | 241 | local function comp(properties, reverse) 242 | return function(a, b) 243 | for _, prop in ipairs(properties) do 244 | local a_val = a[prop] 245 | local b_val = b[prop] 246 | if a_val and b_val and type(a_val) ~= 'table' and a_val ~= b_val then 247 | if reverse[prop] then 248 | return a_val < b_val 249 | else 250 | return a_val > b_val 251 | end 252 | end 253 | end 254 | return false 255 | end 256 | end 257 | 258 | if #sort_video > 0 then 259 | table.sort(video_formats, comp(sort_video, reverse_video)) 260 | end 261 | if #sort_audio > 0 then 262 | table.sort(audio_formats, comp(sort_audio, reverse_audio)) 263 | end 264 | 265 | local function scale_filesize(size) 266 | if size == nil then 267 | return "" 268 | end 269 | size = tonumber(size) 270 | 271 | local counter = 0 272 | while size > 1024 do 273 | size = size / 1024 274 | counter = counter + 1 275 | end 276 | 277 | if counter >= 3 then return string.format("%.1fGiB", size) 278 | elseif counter >= 2 then return string.format("%.1fMiB", size) 279 | elseif counter >= 1 then return string.format("%.1fKiB", size) 280 | else return string.format("%.1fB ", size) 281 | end 282 | end 283 | 284 | local function scale_bitrate(br) 285 | if br == nil then 286 | return "" 287 | end 288 | br = tonumber(br) 289 | 290 | local counter = 0 291 | while br > 1000 do 292 | br = br / 1000 293 | counter = counter + 1 294 | end 295 | 296 | if counter >= 2 then return string.format("%.1fGbps", br) 297 | elseif counter >= 1 then return string.format("%.1fMbps", br) 298 | else return string.format("%.1fKbps", br) 299 | end 300 | end 301 | 302 | local function format_special_fields(format) 303 | local size_prefix = not format.filesize and format.filesize_approx and "~" or "" 304 | format.size = (size_prefix) .. scale_filesize(format.size) 305 | format.frame_rate = format.fps and format.fps .. "fps" or "" 306 | format.bitrate_total = scale_bitrate(format.tbr) 307 | format.bitrate_video = scale_bitrate(format.vbr) 308 | format.bitrate_audio = scale_bitrate(format.abr) 309 | format.codec_video = format.vcodec == nil and "unknown" or format.vcodec == "none" and "" or format.vcodec 310 | format.codec_audio = format.acodec == nil and "unknown" or format.acodec == "none" and "" or format.acodec 311 | format.audio_sample_rate = format.asr and tostring(format.asr) .. "Hz" or "" 312 | end 313 | 314 | for _, format in ipairs(all_formats) do 315 | format_special_fields(format) 316 | end 317 | 318 | local function format_table(formats, columns) 319 | local function calc_shown_columns() 320 | local display_col = {} 321 | local column_widths = {} 322 | local column_values = {} 323 | local columns, column_align_left = strip_minus(columns) 324 | 325 | for _, format in pairs(formats) do 326 | for col, prop in ipairs(columns) do 327 | local label = tostring(format[prop] or "") 328 | format[prop] = label 329 | 330 | if not column_widths[col] or column_widths[col] < label:len() then 331 | column_widths[col] = label:len() 332 | end 333 | 334 | column_values[col] = column_values[col] or label 335 | display_col[col] = display_col[col] or (column_values[col] ~= label) 336 | end 337 | end 338 | 339 | local show_columns = {} 340 | for i, width in ipairs(column_widths) do 341 | if width > 0 and not opts.hide_identical_columns or display_col[i] then 342 | local prop = columns[i] 343 | show_columns[#show_columns + 1] = { 344 | prop = prop, 345 | width = width, 346 | align_left = column_align_left[prop] 347 | } 348 | end 349 | end 350 | return show_columns 351 | end 352 | 353 | local show_columns = calc_shown_columns() 354 | 355 | local spacing = 2 356 | local res = {} 357 | for _, f in ipairs(formats) do 358 | local row = '' 359 | for i, column in ipairs(show_columns) do 360 | -- lua errors out with width > 99 ("invalid conversion specification") 361 | local width = math.min(column.width * (column.align_left and -1 or 1), 99) 362 | row = row .. (i > 1 and string.format('%' .. spacing .. 's', '') or '') 363 | .. string.format('%' .. width .. 's', f[column.prop] or "") 364 | end 365 | res[#res + 1] = { label = row:gsub('%s+$', ''), format = f.format_id } 366 | end 367 | return res 368 | end 369 | 370 | local columns_video = string_split(opts.columns_video, ',') 371 | local columns_audio = string_split(opts.columns_audio, ',') 372 | local vres = format_table(video_formats, columns_video) 373 | local ares = format_table(audio_formats, columns_audio) 374 | return vres, ares, vfmt, afmt 375 | end 376 | 377 | local function get_url() 378 | local path = mp.get_property("path") 379 | if not path then return nil end 380 | path = string.gsub(path, "ytdl://", "") -- Strip possible ytdl:// prefix. 381 | 382 | local function is_url(s) 383 | -- adapted the regex from 384 | -- https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url 385 | return nil ~= 386 | string.match(path, 387 | "^[%w]-://[-a-zA-Z0-9@:%._\\+~#=]+%." .. 388 | "[a-zA-Z0-9()][a-zA-Z0-9()]?[a-zA-Z0-9()]?[a-zA-Z0-9()]?[a-zA-Z0-9()]?[a-zA-Z0-9()]?" .. 389 | "[-a-zA-Z0-9()@:%_\\+.~#?&/=]*") 390 | end 391 | 392 | return is_url(path) and path or nil 393 | end 394 | 395 | local uosc = false 396 | local url_data = {} 397 | local function uosc_set_format_counts() 398 | if not uosc then return end 399 | 400 | local new_path = get_url() 401 | if not new_path then return end 402 | 403 | local data = url_data[new_path] 404 | if data then 405 | mp.commandv('script-message-to', 'uosc', 'set', 'vformats', #data.voptions) 406 | mp.commandv('script-message-to', 'uosc', 'set', 'aformats', #data.aoptions) 407 | else 408 | mp.commandv('script-message-to', 'uosc', 'set', 'vformats', 0) 409 | mp.commandv('script-message-to', 'uosc', 'set', 'aformats', 0) 410 | end 411 | end 412 | 413 | local function process_json_string(url, json) 414 | local json, err = utils.parse_json(json) 415 | 416 | if (json == nil) then 417 | mp.osd_message("fetching formats failed...", 2) 418 | if err == nil then err = "unexpected error occurred" end 419 | msg.error("failed to parse JSON data: " .. err) 420 | return 421 | end 422 | 423 | if json.formats == nil then 424 | return 425 | end 426 | 427 | local vres, ares, vfmt, afmt = process_json(json) 428 | url_data[url] = { voptions = vres, aoptions = ares, vfmt = vfmt, afmt = afmt } 429 | uosc_set_format_counts() 430 | return vres, ares, vfmt, afmt 431 | end 432 | 433 | local function download_formats(url) 434 | 435 | if opts.fetch_on_start and not opts.start_with_menu then 436 | msg.info("fetching available formats with youtube-dl...") 437 | else 438 | mp.osd_message("fetching available formats with youtube-dl...", 60) 439 | end 440 | 441 | if not (ytdl.searched) then 442 | local ytdl_mcd = mp.find_config_file(opts.ytdl_ver) 443 | if not (ytdl_mcd == nil) then 444 | msg.verbose("found youtube-dl at: " .. ytdl_mcd) 445 | ytdl.path = ytdl_mcd 446 | end 447 | ytdl.searched = true 448 | end 449 | 450 | local function exec(args) 451 | msg.debug("Running: " .. table.concat(args, " ")) 452 | local ret = mp.command_native({ 453 | name = "subprocess", 454 | args = args, 455 | capture_stdout = true, 456 | capture_stderr = true 457 | }) 458 | return ret.status, ret.stdout, ret, ret.killed_by_us 459 | end 460 | 461 | local function check_version(ytdl_path) 462 | local command = { 463 | name = "subprocess", 464 | capture_stdout = true, 465 | args = { ytdl_path, "--version" } 466 | } 467 | local version_string = mp.command_native(command).stdout 468 | local year, month, day = string.match(version_string, "(%d+).(%d+).(%d+)") 469 | 470 | -- sanity check 471 | if (tonumber(year) < 2000) or (tonumber(month) > 12) or 472 | (tonumber(day) > 31) then 473 | return 474 | end 475 | local version_ts = os.time { year = year, month = month, day = day } 476 | if (os.difftime(os.time(), version_ts) > 60 * 60 * 24 * 90) then 477 | msg.warn("It appears that your youtube-dl version is severely out of date.") 478 | end 479 | end 480 | 481 | local ytdl_format = mp.get_property("ytdl-format") 482 | local command = nil 483 | if (ytdl_format == nil or ytdl_format == "") then 484 | command = { ytdl.path, "--no-warnings", "--no-playlist", "-J", url } 485 | else 486 | command = { ytdl.path, "--no-warnings", "--no-playlist", "-J", "-f", ytdl_format, url } 487 | end 488 | 489 | msg.verbose("calling youtube-dl with command: " .. table.concat(command, " ")) 490 | 491 | local es, json, result, aborted = exec(command) 492 | 493 | if aborted then 494 | return 495 | end 496 | 497 | if (es ~= 0) or (json == "") then 498 | json = nil 499 | end 500 | 501 | if (json == nil) then 502 | mp.osd_message("fetching formats failed...", 2) 503 | msg.verbose("status:", es) 504 | msg.verbose("reason:", result.error_string) 505 | msg.verbose("stdout:", result.stdout) 506 | msg.verbose("stderr:", result.stderr) 507 | 508 | -- trim our stderr to avoid spurious newlines 509 | local ytdl_err = result.stderr:gsub("^%s*(.-)%s*$", "%1") 510 | msg.error(ytdl_err) 511 | local err = "youtube-dl failed: " 512 | if result.error_string and result.error_string == "init" then 513 | err = err .. "not found or not enough permissions" 514 | elseif not result.killed_by_us then 515 | err = err .. "unexpected error occurred" 516 | else 517 | err = string.format("%s returned '%d'", err, es) 518 | end 519 | msg.error(err) 520 | if string.find(ytdl_err, "yt%-dl%.org/bug") then 521 | check_version(ytdl.path) 522 | end 523 | return 524 | end 525 | 526 | msg.verbose("youtube-dl succeeded!") 527 | mp.osd_message("", 0) 528 | 529 | local vres, ares, vfmt, afmt = process_json_string(url, json) 530 | return vres, ares, vfmt, afmt 531 | end 532 | 533 | local function send_formats_to(type, url, script_name, options, format_id) 534 | mp.commandv('script-message-to', script_name, type .. '_formats', 535 | url, utils.format_json(options or {}), format_id or '') 536 | end 537 | 538 | local queue_callback_video = {} 539 | local queue_callback_audio = {} 540 | local function get_formats() 541 | 542 | local url = get_url() 543 | if url == nil then 544 | return 545 | end 546 | 547 | if url_data[url] then 548 | local data = url_data[url] 549 | return data.voptions, data.aoptions, data.vfmt, data.afmt, url 550 | end 551 | 552 | if opts.fetch_formats == false then 553 | local vres = {} 554 | for i, v in ipairs(opts.quality_strings) do 555 | for k, v2 in pairs(v) do 556 | vres[i] = { label = k, format = v2 } 557 | end 558 | end 559 | url_data[url] = { voptions = vres, aoptions = {}, vfmt = nil, afmt = nil } 560 | return vres, {}, nil, nil, url 561 | end 562 | 563 | local vres, ares, vfmt, afmt = download_formats(url) 564 | 565 | for _, script_name in ipairs(queue_callback_video[url] or {}) do 566 | send_formats_to('video', url, script_name, vres, vfmt) 567 | end 568 | for _, script_name in ipairs(queue_callback_audio[url] or {}) do 569 | send_formats_to('audio', url, script_name, ares, afmt) 570 | end 571 | 572 | queue_callback_video[url] = nil 573 | queue_callback_audio[url] = nil 574 | return vres, ares, vfmt, afmt, url 575 | end 576 | 577 | local function format_string(vfmt, afmt) 578 | if vfmt and afmt then 579 | return vfmt .. "+" .. afmt 580 | elseif vfmt then 581 | return vfmt 582 | elseif afmt then 583 | return afmt 584 | else 585 | return "" 586 | end 587 | end 588 | 589 | local function set_format(url, vfmt, afmt) 590 | if (url_data[url].vfmt ~= vfmt or url_data[url].afmt ~= afmt) then 591 | url_data[url].afmt = afmt 592 | url_data[url].vfmt = vfmt 593 | if url == mp.get_property("path") then 594 | mp.set_property("ytdl-format", format_string(vfmt, afmt)) 595 | reload_resume() 596 | end 597 | end 598 | end 599 | 600 | local destroyer = nil 601 | local function show_menu(isvideo) 602 | 603 | if destroyer then 604 | destroyer() 605 | end 606 | 607 | local voptions, aoptions, vfmt, afmt, url = get_formats() 608 | 609 | local options 610 | local fmt 611 | if isvideo then 612 | options = voptions 613 | fmt = vfmt 614 | else 615 | options = aoptions 616 | fmt = afmt 617 | end 618 | 619 | if options == nil then 620 | if uosc then 621 | if isvideo then 622 | mp.commandv('script-binding', 'uosc/video') 623 | else 624 | mp.commandv('script-binding', 'uosc/audio') 625 | end 626 | end 627 | 628 | return 629 | end 630 | 631 | msg.verbose("current ytdl-format: " .. format_string(vfmt, afmt)) 632 | 633 | local active = 0 634 | local selected = 1 635 | --set the cursor to the current format 636 | if fmt then 637 | for i, v in ipairs(options) do 638 | if v.format == fmt then 639 | active = i 640 | selected = active 641 | break 642 | end 643 | end 644 | else 645 | active = #options + 1 646 | selected = active 647 | end 648 | 649 | if uosc then 650 | local menu = { 651 | title = isvideo and 'Video Formats' or 'Audio Formats', 652 | items = {}, 653 | type = (isvideo and 'video' or 'audio') .. '_formats', 654 | } 655 | for i, option in ipairs(options) do 656 | menu.items[i] = { 657 | title = option.label, 658 | active = i == active, 659 | value = { 660 | 'script-message-to', 661 | 'quality_menu', 662 | (isvideo and 'video' or 'audio') .. '-format-set', 663 | url, 664 | option.format 665 | } 666 | } 667 | end 668 | menu.items[#menu.items + 1] = { 669 | title = 'None', 670 | value = { 671 | 'script-message-to', 672 | 'quality_menu', 673 | (isvideo and 'video' or 'audio') .. '-format-set', 674 | url 675 | } 676 | } 677 | local json = utils.format_json(menu) 678 | mp.commandv('script-message-to', 'uosc', 'open-menu', json) 679 | return 680 | end 681 | 682 | local function choose_prefix(i) 683 | if i == selected and i == active then return opts.selected_and_active 684 | elseif i == selected then return opts.selected_and_inactive end 685 | 686 | if i ~= selected and i == active then return opts.unselected_and_active 687 | elseif i ~= selected then return opts.unselected_and_inactive end 688 | return "> " --shouldn't get here. 689 | end 690 | 691 | local width, height 692 | local margin_top, margin_bottom = 0, 0 693 | local num_options = #options + 1 694 | 695 | local function get_scrolled_lines() 696 | local output_height = height - opts.text_padding_y * 2 - margin_top * height - margin_bottom * height 697 | local screen_lines = math.max(math.floor(output_height / opts.font_size), 1) 698 | local max_scroll = math.max(num_options - screen_lines, 0) 699 | return math.min(math.max(selected - math.ceil(screen_lines / 2), 0), max_scroll) 700 | end 701 | 702 | local function draw_menu() 703 | local ass = assdraw.ass_new() 704 | 705 | if opts.curtain_opacity > 0 then 706 | local alpha = 255 - math.ceil(255 * opts.curtain_opacity) 707 | ass.text = string.format('{\\pos(0,0)\\rDefault\\an7\\1c&H000000&\\alpha&H%X&}', alpha) 708 | ass:draw_start() 709 | ass:rect_cw(0, 0, width, height) 710 | ass:draw_stop() 711 | ass:new_event() 712 | end 713 | 714 | local scrolled_lines = get_scrolled_lines() 715 | local pos_y = opts.shift_y + margin_top * height + opts.text_padding_y - scrolled_lines * opts.font_size 716 | ass:pos(opts.shift_x + opts.text_padding_x, pos_y) 717 | local clip_top = math.floor(margin_top * height + 0.5) 718 | local clip_bottom = math.floor((1 - margin_bottom) * height + 0.5) 719 | local clipping_coordinates = '0,' .. clip_top .. ',' .. width .. ',' .. clip_bottom 720 | ass:append(opts.style_ass_tags .. '{\\q2\\clip(' .. clipping_coordinates .. ')}') 721 | 722 | if #options > 0 then 723 | for i, v in ipairs(options) do 724 | ass:append(choose_prefix(i) .. v.label .. "\\N") 725 | end 726 | ass:append(choose_prefix(#options + 1) .. "None") 727 | else 728 | ass:append("no formats found") 729 | end 730 | 731 | mp.set_osd_ass(width, height, ass.text) 732 | end 733 | 734 | local function update_dimensions() 735 | local _, h, aspect = mp.get_osd_size() 736 | if opts.scale_playlist_by_window then h = 720 end 737 | height = h 738 | width = h * aspect 739 | draw_menu() 740 | end 741 | 742 | local function update_margins() 743 | local shared_props = mp.get_property_native('shared-script-properties') 744 | local val = shared_props['osc-margins'] 745 | if val then 746 | -- formatted as "%f,%f,%f,%f" with left, right, top, bottom, each 747 | -- value being the border size as ratio of the window size (0.0-1.0) 748 | local vals = {} 749 | for v in string.gmatch(val, "[^,]+") do 750 | vals[#vals + 1] = tonumber(v) 751 | end 752 | margin_top = vals[3] -- top 753 | margin_bottom = vals[4] -- bottom 754 | else 755 | margin_top = 0 756 | margin_bottom = 0 757 | end 758 | draw_menu() 759 | end 760 | 761 | update_dimensions() 762 | update_margins() 763 | mp.observe_property('osd-dimensions', 'native', update_dimensions) 764 | mp.observe_property('shared-script-properties', 'native', update_margins) 765 | 766 | local timeout = nil 767 | 768 | local function selected_move(amt) 769 | selected = selected + amt 770 | if selected < 1 then selected = num_options 771 | elseif selected > num_options then selected = 1 end 772 | if timeout then 773 | timeout:kill() 774 | timeout:resume() 775 | end 776 | draw_menu() 777 | end 778 | 779 | local function bind_keys(keys, name, func, opts) 780 | if not keys then 781 | mp.add_forced_key_binding(keys, name, func, opts) 782 | return 783 | end 784 | local i = 1 785 | for key in keys:gmatch("[^%s]+") do 786 | local prefix = i == 1 and '' or i 787 | mp.add_forced_key_binding(key, name .. prefix, func, opts) 788 | i = i + 1 789 | end 790 | end 791 | 792 | local function unbind_keys(keys, name) 793 | if not keys then 794 | mp.remove_key_binding(name) 795 | return 796 | end 797 | local i = 1 798 | for key in keys:gmatch("[^%s]+") do 799 | local prefix = i == 1 and '' or i 800 | mp.remove_key_binding(name .. prefix) 801 | i = i + 1 802 | end 803 | end 804 | 805 | local function destroy() 806 | if timeout then 807 | timeout:kill() 808 | end 809 | mp.set_osd_ass(0, 0, "") 810 | unbind_keys(opts.up_binding, "move_up") 811 | unbind_keys(opts.down_binding, "move_down") 812 | unbind_keys(opts.select_binding, "select") 813 | unbind_keys(opts.close_menu_binding, "close") 814 | mp.unobserve_property(update_dimensions) 815 | mp.unobserve_property(update_margins) 816 | destroyer = nil 817 | end 818 | 819 | if opts.menu_timeout > 0 then 820 | timeout = mp.add_periodic_timer(opts.menu_timeout, destroy) 821 | end 822 | destroyer = destroy 823 | 824 | bind_keys(opts.up_binding, "move_up", function() selected_move(-1) end, { repeatable = true }) 825 | bind_keys(opts.down_binding, "move_down", function() selected_move(1) end, { repeatable = true }) 826 | if #options > 0 then 827 | bind_keys(opts.select_binding, "select", function() 828 | destroy() 829 | if selected == active then return end 830 | 831 | fmt = options[selected] and options[selected].format or nil 832 | if isvideo then 833 | vfmt = fmt 834 | else 835 | afmt = fmt 836 | end 837 | set_format(url, vfmt, afmt) 838 | end) 839 | end 840 | bind_keys(opts.close_menu_binding, "close", destroy) --close menu using ESC 841 | mp.osd_message("", 0) 842 | draw_menu() 843 | end 844 | 845 | local ui_callback = {} 846 | 847 | local function video_formats_toggle() 848 | if #ui_callback > 0 then 849 | for _, name in ipairs(ui_callback) do 850 | mp.commandv('script-message-to', name, 'video-formats-menu') 851 | end 852 | else 853 | show_menu(true) 854 | end 855 | end 856 | 857 | local function audio_formats_toggle() 858 | if #ui_callback > 0 then 859 | for _, name in ipairs(ui_callback) do 860 | mp.commandv('script-message-to', name, 'audio-formats-menu') 861 | end 862 | else 863 | show_menu(false) 864 | end 865 | end 866 | 867 | -- keybind to launch menu 868 | mp.add_key_binding(nil, "video_formats_toggle", video_formats_toggle) 869 | mp.add_key_binding(nil, "audio_formats_toggle", audio_formats_toggle) 870 | mp.add_key_binding(nil, "reload", reload_resume) 871 | 872 | local original_format = mp.get_property("ytdl-format") 873 | local path = nil 874 | local function file_start() 875 | uosc_set_format_counts() 876 | 877 | local new_path = get_url() 878 | if not new_path then return end 879 | 880 | local data = url_data[new_path] 881 | 882 | if opts.reset_format and path and new_path ~= path then 883 | if data then 884 | msg.verbose("setting previously set format") 885 | mp.set_property("ytdl-format", format_string(data.vfmt, data.afmt)) 886 | else 887 | msg.verbose("setting original format") 888 | mp.set_property("ytdl-format", original_format) 889 | end 890 | end 891 | if opts.start_with_menu and new_path ~= path then 892 | video_formats_toggle() 893 | elseif opts.fetch_on_start and not data then 894 | download_formats(new_path) 895 | end 896 | path = new_path 897 | end 898 | 899 | mp.register_event("start-file", file_start) 900 | 901 | mp.register_script_message('video-formats-get', function(url, script_name) 902 | local data = url_data[url] 903 | if data then 904 | send_formats_to('video', url, script_name, data.voptions, data.vfmt) 905 | else 906 | local queue = queue_callback_video[url] or {} 907 | queue[#queue + 1] = script_name 908 | queue_callback_video[url] = queue 909 | get_formats() 910 | end 911 | end) 912 | 913 | mp.register_script_message('audio-formats-get', function(url, script_name) 914 | local data = url_data[url] 915 | if data then 916 | send_formats_to('audio', url, script_name, data.aoptions, data.afmt) 917 | else 918 | local queue = queue_callback_audio[url] or {} 919 | queue[#queue + 1] = script_name 920 | queue_callback_audio[url] = queue 921 | get_formats() 922 | end 923 | end) 924 | 925 | mp.register_script_message('video-format-set', function(url, format_id) 926 | set_format(url, format_id, url_data[url].afmt) 927 | end) 928 | 929 | mp.register_script_message('audio-format-set', function(url, format_id) 930 | set_format(url, url_data[url].vfmt, format_id) 931 | end) 932 | 933 | mp.register_script_message('register-ui', function(script_name) 934 | ui_callback[#ui_callback + 1] = script_name 935 | end) 936 | 937 | -- check if uosc is running 938 | mp.register_script_message('uosc-version', function(version) 939 | version = tonumber((version:gsub('%.', ''))) 940 | ---@diagnostic disable-next-line: cast-local-type 941 | uosc = version and version >= 400 942 | uosc_set_format_counts() 943 | end) 944 | mp.commandv('script-message-to', 'uosc', 'get-version', mp.get_script_name()) 945 | -------------------------------------------------------------------------------- /scripts/seek-to.lua: -------------------------------------------------------------------------------- 1 | local assdraw = require 'mp.assdraw' 2 | local active = false 3 | local cursor_position = 1 4 | local time_scale = {60*60*10, 60*60, 60*10, 60, 10, 1, 0.1, 0.01, 0.001} 5 | 6 | local ass_begin = mp.get_property("osd-ass-cc/0") 7 | local ass_end = mp.get_property("osd-ass-cc/1") 8 | 9 | local history = { {} } 10 | for i = 1, 9 do 11 | history[1][i] = 0 12 | end 13 | local history_position = 1 14 | 15 | local timer = nil 16 | local timer_duration = 3 17 | 18 | function show_seeker() 19 | local prepend_char = {'','',':','',':','','.','',''} 20 | local str = '' 21 | for i = 1, 9 do 22 | str = str .. prepend_char[i] 23 | if i == cursor_position then 24 | str = str .. '{\\b1}' .. history[history_position][i] .. '{\\r}' 25 | else 26 | str = str .. history[history_position][i] 27 | end 28 | end 29 | mp.osd_message("Seek to: " .. ass_begin .. str .. ass_end, timer_duration) 30 | end 31 | 32 | function copy_history_to_last() 33 | if history_position ~= #history then 34 | for i = 1, 9 do 35 | history[#history][i] = history[history_position][i] 36 | end 37 | history_position = #history 38 | end 39 | end 40 | 41 | function change_number(i) 42 | if (cursor_position == 3 or cursor_position == 5) and i >= 6 then 43 | return 44 | end 45 | if history[history_position][cursor_position] ~= i then 46 | copy_history_to_last() 47 | history[#history][cursor_position] = i 48 | end 49 | shift_cursor(false) 50 | end 51 | 52 | function shift_cursor(left) 53 | if left then 54 | cursor_position = math.max(1, cursor_position - 1) 55 | else 56 | cursor_position = math.min(cursor_position + 1, 9) 57 | end 58 | end 59 | 60 | function current_time_as_sec(time) 61 | local sec = 0 62 | for i = 1, 9 do 63 | sec = sec + time_scale[i] * time[i] 64 | end 65 | return sec 66 | end 67 | 68 | function time_equal(lhs, rhs) 69 | for i = 1, 9 do 70 | if lhs[i] ~= rhs[i] then 71 | return false 72 | end 73 | end 74 | return true 75 | end 76 | 77 | function seek_to() 78 | copy_history_to_last() 79 | mp.commandv("osd-bar", "seek", current_time_as_sec(history[history_position]), "absolute") 80 | if #history == 1 or not time_equal(history[history_position], history[#history - 1]) then 81 | history[#history + 1] = {} 82 | history_position = #history 83 | end 84 | for i = 1, 9 do 85 | history[#history][i] = 0 86 | end 87 | end 88 | 89 | function backspace() 90 | if cursor_position ~= 9 or current_time[9] == 0 then 91 | shift_cursor(true) 92 | end 93 | if history[history_position][cursor_position] ~= 0 then 94 | copy_history_to_last() 95 | history[#history][cursor_position] = 0 96 | end 97 | end 98 | 99 | function history_move(up) 100 | if up then 101 | history_position = math.max(1, history_position - 1) 102 | else 103 | history_position = math.min(history_position + 1, #history) 104 | end 105 | end 106 | 107 | local key_mappings = { 108 | LEFT = function() shift_cursor(true) show_seeker() end, 109 | RIGHT = function() shift_cursor(false) show_seeker() end, 110 | UP = function() history_move(true) show_seeker() end, 111 | DOWN = function() history_move(false) show_seeker() end, 112 | BS = function() backspace() show_seeker() end, 113 | ESC = function() set_inactive() end, 114 | ENTER = function() seek_to() set_inactive() end 115 | } 116 | for i = 0, 9 do 117 | local func = function() change_number(i) show_seeker() end 118 | key_mappings[string.format("KP%d", i)] = func 119 | key_mappings[string.format("%d", i)] = func 120 | end 121 | 122 | function set_active() 123 | if not mp.get_property("seekable") then return end 124 | local duration = mp.get_property_number("duration") 125 | if duration ~= nil then 126 | for i = 1, 9 do 127 | if duration > time_scale[i] then 128 | cursor_position = i 129 | break 130 | end 131 | end 132 | end 133 | for key, func in pairs(key_mappings) do 134 | mp.add_forced_key_binding(key, "seek-to-"..key, func) 135 | end 136 | show_seeker() 137 | timer = mp.add_periodic_timer(timer_duration, show_seeker) 138 | active = true 139 | end 140 | 141 | function set_inactive() 142 | mp.osd_message("") 143 | for key, _ in pairs(key_mappings) do 144 | mp.remove_key_binding("seek-to-"..key) 145 | end 146 | timer:kill() 147 | active = false 148 | end 149 | 150 | mp.add_key_binding(nil, "toggle-seeker", function() if active then set_inactive() else set_active() end end) 151 | -------------------------------------------------------------------------------- /shaders/CfL_Prediction.glsl: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2023 João Chrisóstomo 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | //!HOOK CHROMA 24 | //!BIND LUMA 25 | //!BIND HOOKED 26 | //!SAVE LUMA_LOWRES 27 | //!WIDTH CHROMA.w 28 | //!HEIGHT CHROMA.h 29 | //!WHEN CHROMA.w LUMA.w < 30 | //!DESC Chroma From Luma Prediction (Downscaling Luma) 31 | 32 | vec4 hook() { 33 | vec2 start = ceil((LUMA_pos - CHROMA_pt) * LUMA_size - 0.5); 34 | vec2 end = floor((LUMA_pos + CHROMA_pt) * LUMA_size - 0.5); 35 | 36 | float luma_pix = 0.0; 37 | float w = 0.0; 38 | float d = 0.0; 39 | float wt = 0.0; 40 | float val = 0.0; 41 | vec2 pos = LUMA_pos; 42 | 43 | for (float dx = start.x; dx <= end.x; dx++) { 44 | for (float dy = start.y; dy <= end.y; dy++) { 45 | pos = LUMA_pt * vec2(dx + 0.5, dy + 0.5); 46 | d = length((pos - LUMA_pos) * CHROMA_size); 47 | w = exp(-2.0 * pow(d, 2.0)); 48 | luma_pix = LUMA_tex(pos).x; 49 | val += w * luma_pix; 50 | wt += w; 51 | } 52 | } 53 | 54 | vec4 output_pix = vec4(val / wt, 0.0, 0.0, 1.0); 55 | return output_pix; 56 | } 57 | 58 | //!HOOK CHROMA 59 | //!BIND CHROMA 60 | //!BIND LUMA 61 | //!BIND LUMA_LOWRES 62 | //!WHEN CHROMA.w LUMA.w < 63 | //!WIDTH LUMA.w 64 | //!HEIGHT LUMA.h 65 | //!OFFSET ALIGN 66 | //!DESC Chroma From Luma Prediction (Upscaling Chroma) 67 | 68 | float comp_wd(vec2 distance) { 69 | float d = length(distance); 70 | if (d < 1.0) { 71 | return (6.0 + d * d * (-15.0 + d * 9.0)) / 6.0; 72 | } else if (d < 2.0) { 73 | return (12.0 + d * (-24.0 + d * (15.0 + d * -3.0))) / 6.0; 74 | } else { 75 | return 0.0; 76 | } 77 | } 78 | 79 | vec4 hook() { 80 | vec4 output_pix = vec4(0.0, 0.0, 0.0, 1.0); 81 | float luma_zero = LUMA_texOff(0.0).x; 82 | 83 | vec2 pp = CHROMA_pos * CHROMA_size - vec2(0.5); 84 | vec2 fp = floor(pp); 85 | pp -= fp; 86 | 87 | vec2 chroma_pixels[12]; 88 | chroma_pixels[0] = CHROMA_tex(vec2((fp + vec2(0.5, -0.5)) * CHROMA_pt)).xy; 89 | chroma_pixels[1] = CHROMA_tex(vec2((fp + vec2(1.5, -0.5)) * CHROMA_pt)).xy; 90 | chroma_pixels[2] = CHROMA_tex(vec2((fp + vec2(-0.5, 0.5)) * CHROMA_pt)).xy; 91 | chroma_pixels[3] = CHROMA_tex(vec2((fp + vec2( 0.5, 0.5)) * CHROMA_pt)).xy; 92 | chroma_pixels[4] = CHROMA_tex(vec2((fp + vec2( 1.5, 0.5)) * CHROMA_pt)).xy; 93 | chroma_pixels[5] = CHROMA_tex(vec2((fp + vec2( 2.5, 0.5)) * CHROMA_pt)).xy; 94 | chroma_pixels[6] = CHROMA_tex(vec2((fp + vec2(-0.5, 1.5)) * CHROMA_pt)).xy; 95 | chroma_pixels[7] = CHROMA_tex(vec2((fp + vec2( 0.5, 1.5)) * CHROMA_pt)).xy; 96 | chroma_pixels[8] = CHROMA_tex(vec2((fp + vec2( 1.5, 1.5)) * CHROMA_pt)).xy; 97 | chroma_pixels[9] = CHROMA_tex(vec2((fp + vec2( 2.5, 1.5)) * CHROMA_pt)).xy; 98 | chroma_pixels[10] = CHROMA_tex(vec2((fp + vec2(0.5, 2.5) ) * CHROMA_pt)).xy; 99 | chroma_pixels[11] = CHROMA_tex(vec2((fp + vec2(1.5, 2.5) ) * CHROMA_pt)).xy; 100 | 101 | float luma_pixels[12]; 102 | luma_pixels[0] = LUMA_LOWRES_tex(vec2((fp + vec2(0.5, -0.5)) * CHROMA_pt)).x; 103 | luma_pixels[1] = LUMA_LOWRES_tex(vec2((fp + vec2(1.5, -0.5)) * CHROMA_pt)).x; 104 | luma_pixels[2] = LUMA_LOWRES_tex(vec2((fp + vec2(-0.5, 0.5)) * CHROMA_pt)).x; 105 | luma_pixels[3] = LUMA_LOWRES_tex(vec2((fp + vec2( 0.5, 0.5)) * CHROMA_pt)).x; 106 | luma_pixels[4] = LUMA_LOWRES_tex(vec2((fp + vec2( 1.5, 0.5)) * CHROMA_pt)).x; 107 | luma_pixels[5] = LUMA_LOWRES_tex(vec2((fp + vec2( 2.5, 0.5)) * CHROMA_pt)).x; 108 | luma_pixels[6] = LUMA_LOWRES_tex(vec2((fp + vec2(-0.5, 1.5)) * CHROMA_pt)).x; 109 | luma_pixels[7] = LUMA_LOWRES_tex(vec2((fp + vec2( 0.5, 1.5)) * CHROMA_pt)).x; 110 | luma_pixels[8] = LUMA_LOWRES_tex(vec2((fp + vec2( 1.5, 1.5)) * CHROMA_pt)).x; 111 | luma_pixels[9] = LUMA_LOWRES_tex(vec2((fp + vec2( 2.5, 1.5)) * CHROMA_pt)).x; 112 | luma_pixels[10] = LUMA_LOWRES_tex(vec2((fp + vec2(0.5, 2.5) ) * CHROMA_pt)).x; 113 | luma_pixels[11] = LUMA_LOWRES_tex(vec2((fp + vec2(1.5, 2.5) ) * CHROMA_pt)).x; 114 | 115 | vec2 chroma_min = vec2(1e8); 116 | chroma_min = min(chroma_min, chroma_pixels[3]); 117 | chroma_min = min(chroma_min, chroma_pixels[4]); 118 | chroma_min = min(chroma_min, chroma_pixels[7]); 119 | chroma_min = min(chroma_min, chroma_pixels[8]); 120 | 121 | vec2 chroma_max = vec2(1e-8); 122 | chroma_max = max(chroma_max, chroma_pixels[3]); 123 | chroma_max = max(chroma_max, chroma_pixels[4]); 124 | chroma_max = max(chroma_max, chroma_pixels[7]); 125 | chroma_max = max(chroma_max, chroma_pixels[8]); 126 | 127 | float wd[12]; 128 | wd[0] = comp_wd(vec2( 0.0,-1.0) - pp); 129 | wd[1] = comp_wd(vec2( 1.0,-1.0) - pp); 130 | wd[2] = comp_wd(vec2(-1.0, 0.0) - pp); 131 | wd[3] = comp_wd(vec2( 0.0, 0.0) - pp); 132 | wd[4] = comp_wd(vec2( 1.0, 0.0) - pp); 133 | wd[5] = comp_wd(vec2( 2.0, 0.0) - pp); 134 | wd[6] = comp_wd(vec2(-1.0, 1.0) - pp); 135 | wd[7] = comp_wd(vec2( 0.0, 1.0) - pp); 136 | wd[8] = comp_wd(vec2( 1.0, 1.0) - pp); 137 | wd[9] = comp_wd(vec2( 2.0, 1.0) - pp); 138 | wd[10] = comp_wd(vec2( 0.0, 2.0) - pp); 139 | wd[11] = comp_wd(vec2( 1.0, 2.0) - pp); 140 | 141 | float wt = 0.0; 142 | for (int i = 0; i < 12; i++) { 143 | wt += wd[i]; 144 | } 145 | 146 | vec2 ct = vec2(0.0); 147 | for (int i = 0; i < 12; i++) { 148 | ct += wd[i] * chroma_pixels[i]; 149 | } 150 | 151 | vec2 chroma_spatial = ct / wt; 152 | chroma_spatial = clamp(chroma_spatial, chroma_min, chroma_max); 153 | 154 | float luma_avg_4 = 0.0; 155 | luma_avg_4 += luma_pixels[3]; 156 | luma_avg_4 += luma_pixels[4]; 157 | luma_avg_4 += luma_pixels[7]; 158 | luma_avg_4 += luma_pixels[8]; 159 | luma_avg_4 /= 4.0; 160 | 161 | float luma_var_4 = 0.0; 162 | luma_var_4 += pow(luma_pixels[3] - luma_avg_4, 2.0); 163 | luma_var_4 += pow(luma_pixels[4] - luma_avg_4, 2.0); 164 | luma_var_4 += pow(luma_pixels[7] - luma_avg_4, 2.0); 165 | luma_var_4 += pow(luma_pixels[8] - luma_avg_4, 2.0); 166 | 167 | vec2 chroma_avg_4 = vec2(0.0); 168 | chroma_avg_4 += chroma_pixels[3]; 169 | chroma_avg_4 += chroma_pixels[4]; 170 | chroma_avg_4 += chroma_pixels[7]; 171 | chroma_avg_4 += chroma_pixels[8]; 172 | chroma_avg_4 /= 4.0; 173 | 174 | vec2 luma_chroma_cov_4 = vec2(0.0); 175 | luma_chroma_cov_4 += (luma_pixels[3] - luma_avg_4) * (chroma_pixels[3] - chroma_avg_4); 176 | luma_chroma_cov_4 += (luma_pixels[4] - luma_avg_4) * (chroma_pixels[4] - chroma_avg_4); 177 | luma_chroma_cov_4 += (luma_pixels[7] - luma_avg_4) * (chroma_pixels[7] - chroma_avg_4); 178 | luma_chroma_cov_4 += (luma_pixels[8] - luma_avg_4) * (chroma_pixels[8] - chroma_avg_4); 179 | 180 | vec2 alpha_4 = luma_chroma_cov_4 / max(luma_var_4, 1e-6); 181 | vec2 beta_4 = chroma_avg_4 - alpha_4 * luma_avg_4; 182 | 183 | vec2 chroma_pred_4 = alpha_4 * luma_zero + beta_4; 184 | chroma_pred_4 = clamp(chroma_pred_4, 0.0, 1.0); 185 | 186 | float luma_avg_12 = 0.0; 187 | for(int i = 0; i < 12; i++) { 188 | luma_avg_12 += luma_pixels[i]; 189 | } 190 | luma_avg_12 /= 12.0; 191 | 192 | float luma_var_12 = 0.0; 193 | for(int i = 0; i < 12; i++) { 194 | luma_var_12 += pow(luma_pixels[i] - luma_avg_12, 2.0); 195 | } 196 | 197 | vec2 chroma_avg_12 = vec2(0.0); 198 | for(int i = 0; i < 12; i++) { 199 | chroma_avg_12 += chroma_pixels[i]; 200 | } 201 | chroma_avg_12 /= 12.0; 202 | 203 | vec2 chroma_var_12 = vec2(0.0); 204 | for(int i = 0; i < 12; i++) { 205 | chroma_var_12 += pow(chroma_pixels[i] - chroma_avg_12, vec2(2.0)); 206 | } 207 | 208 | vec2 luma_chroma_cov_12 = vec2(0.0); 209 | for(int i = 0; i < 12; i++) { 210 | luma_chroma_cov_12 += (luma_pixels[i] - luma_avg_12) * (chroma_pixels[i] - chroma_avg_12); 211 | } 212 | 213 | vec2 corr = abs(luma_chroma_cov_12 / max(sqrt(luma_var_12 * chroma_var_12), 1e-6)); 214 | corr = clamp(corr, 0.0, 1.0); 215 | 216 | vec2 alpha_12 = luma_chroma_cov_12 / max(luma_var_12, 1e-6); 217 | vec2 beta_12 = chroma_avg_12 - alpha_12 * luma_avg_12; 218 | 219 | vec2 chroma_pred_12 = alpha_12 * luma_zero + beta_12; 220 | chroma_pred_12 = clamp(chroma_pred_12, 0.0, 1.0); 221 | 222 | chroma_pred_4 = mix(chroma_spatial, chroma_pred_4, pow(corr, vec2(2.0)) / 2.0); 223 | chroma_pred_12 = mix(chroma_spatial, chroma_pred_12, pow(corr, vec2(2.0)) / 2.0); 224 | output_pix.xy = mix(chroma_pred_4, chroma_pred_12, 0.5); 225 | 226 | // Replace this with chroma_min and chroma_max if you want AR 227 | // output_pix.yz = clamp(output_pix.yz, chroma_min, chroma_max); 228 | output_pix.xy = clamp(output_pix.xy, 0.0, 1.0); 229 | return output_pix; 230 | } -------------------------------------------------------------------------------- /shaders/FastBilateral.glsl: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2023 João Chrisóstomo 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | //!PARAM intensity_coeff 24 | //!TYPE float 25 | //!MINIMUM 0.0 26 | 256.0 27 | 28 | //!HOOK CHROMA 29 | //!BIND CHROMA 30 | //!BIND LUMA 31 | //!WIDTH LUMA.w 32 | //!HEIGHT LUMA.h 33 | //!WHEN CHROMA.w LUMA.w < 34 | //!OFFSET ALIGN 35 | //!DESC Fast Bilateral (Upscaling Chroma) 36 | 37 | float comp_wi(float distance) { 38 | return exp(-intensity_coeff * distance * distance); 39 | } 40 | 41 | vec4 hook() { 42 | vec2 pp = CHROMA_pos * CHROMA_size - vec2(0.5); 43 | vec2 fp = floor(pp); 44 | pp -= fp; 45 | 46 | float luma_00 = LUMA_texOff(0).x; 47 | 48 | vec2 chroma_11 = CHROMA_tex(vec2(fp + vec2(0.5)) * CHROMA_pt).xy; 49 | vec2 chroma_12 = CHROMA_tex(vec2(fp + vec2(0.5, 1.5)) * CHROMA_pt).xy; 50 | vec2 chroma_21 = CHROMA_tex(vec2(fp + vec2(1.5, 0.5)) * CHROMA_pt).xy; 51 | vec2 chroma_22 = CHROMA_tex(vec2(fp + vec2(1.5, 1.5)) * CHROMA_pt).xy; 52 | 53 | float luma_11 = LUMA_tex(vec2(fp + vec2(0.5)) * CHROMA_pt).x; 54 | float luma_12 = LUMA_tex(vec2(fp + vec2(0.5, 1.5)) * CHROMA_pt).x; 55 | float luma_21 = LUMA_tex(vec2(fp + vec2(1.5, 0.5)) * CHROMA_pt).x; 56 | float luma_22 = LUMA_tex(vec2(fp + vec2(1.5, 1.5)) * CHROMA_pt).x; 57 | 58 | float wd11 = (1 - pp.y) * (1 - pp.x); 59 | float wd12 = pp.y * (1 - pp.x); 60 | float wd21 = (1 - pp.y) * pp.x; 61 | float wd22 = pp.y * pp.x; 62 | 63 | float wi11 = comp_wi(abs(luma_00 - luma_11)); 64 | float wi12 = comp_wi(abs(luma_00 - luma_12)); 65 | float wi21 = comp_wi(abs(luma_00 - luma_21)); 66 | float wi22 = comp_wi(abs(luma_00 - luma_22)); 67 | 68 | float w11 = wd11 * wi11; 69 | float w12 = wd12 * wi12; 70 | float w21 = wd21 * wi21; 71 | float w22 = wd22 * wi22; 72 | 73 | vec2 ct = chroma_11 * w11 + chroma_12 * w12 + chroma_21 * w21 + chroma_22 * w22; 74 | float wt = w11 + w12 + w21 + w22; 75 | 76 | vec4 output_pix = vec4(0.0, 0.0, 0.0, 1.0); 77 | output_pix.xy = ct / wt; 78 | return output_pix; 79 | } -------------------------------------------------------------------------------- /shaders/JointBilateral.glsl: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2023 João Chrisóstomo 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | //!HOOK CHROMA 24 | //!BIND LUMA 25 | //!BIND HOOKED 26 | //!SAVE LUMA_LOWRES 27 | //!WHEN CHROMA.w LUMA.w < 28 | //!DESC Joint Bilateral (Downscaling Luma) 29 | 30 | vec4 hook() { 31 | vec2 start = ceil((LUMA_pos - CHROMA_pt) * LUMA_size - 0.5); 32 | vec2 end = floor((LUMA_pos + CHROMA_pt) * LUMA_size - 0.5); 33 | 34 | float luma_pix = 0.0; 35 | float w = 0.0; 36 | float d = 0.0; 37 | float wt = 0.0; 38 | float val = 0.0; 39 | vec2 pos = LUMA_pos; 40 | 41 | for (float dx = start.x; dx <= end.x; dx++) { 42 | for (float dy = start.y; dy <= end.y; dy++) { 43 | pos = LUMA_pt * vec2(dx + 0.5, dy + 0.5); 44 | d = length((pos - LUMA_pos) * CHROMA_size); 45 | w = exp(-2.0 * pow(d, 2.0)); 46 | luma_pix = LUMA_tex(pos).x; 47 | val += w * luma_pix; 48 | wt += w; 49 | } 50 | } 51 | 52 | vec4 output_pix = vec4(val / wt, 0.0, 0.0, 1.0); 53 | return output_pix; 54 | } 55 | 56 | //!PARAM distance_coeff 57 | //!TYPE float 58 | //!MINIMUM 0.0 59 | 4.0 60 | 61 | //!PARAM intensity_coeff 62 | //!TYPE float 63 | //!MINIMUM 0.0 64 | 256.0 65 | 66 | //!HOOK CHROMA 67 | //!BIND CHROMA 68 | //!BIND LUMA 69 | //!BIND LUMA_LOWRES 70 | //!WIDTH LUMA.w 71 | //!HEIGHT LUMA.h 72 | //!WHEN CHROMA.w LUMA.w < 73 | //!OFFSET ALIGN 74 | //!DESC Joint Bilateral (Upscaling Chroma) 75 | 76 | float comp_wd(vec2 distance) { 77 | return exp(-distance_coeff * (distance.x * distance.x + distance.y * distance.y)); 78 | } 79 | 80 | float comp_wi(float distance) { 81 | return exp(-intensity_coeff * distance * distance); 82 | } 83 | 84 | float comp_w(float wd, float wi) { 85 | float w = wd * wi; 86 | // return clamp(w, 1e-32, 1.0); 87 | return w; 88 | } 89 | 90 | vec4 hook() { 91 | vec2 pp = CHROMA_pos * CHROMA_size - vec2(0.5); 92 | vec2 fp = floor(pp); 93 | pp -= fp; 94 | 95 | vec2 chroma_b = CHROMA_tex(vec2((fp + vec2(0.5, -0.5)) * CHROMA_pt)).xy; 96 | vec2 chroma_c = CHROMA_tex(vec2((fp + vec2(1.5, -0.5)) * CHROMA_pt)).xy; 97 | vec2 chroma_e = CHROMA_tex(vec2((fp + vec2(-0.5, 0.5)) * CHROMA_pt)).xy; 98 | vec2 chroma_f = CHROMA_tex(vec2((fp + vec2( 0.5, 0.5)) * CHROMA_pt)).xy; 99 | vec2 chroma_g = CHROMA_tex(vec2((fp + vec2( 1.5, 0.5)) * CHROMA_pt)).xy; 100 | vec2 chroma_h = CHROMA_tex(vec2((fp + vec2( 2.5, 0.5)) * CHROMA_pt)).xy; 101 | vec2 chroma_i = CHROMA_tex(vec2((fp + vec2(-0.5, 1.5)) * CHROMA_pt)).xy; 102 | vec2 chroma_j = CHROMA_tex(vec2((fp + vec2( 0.5, 1.5)) * CHROMA_pt)).xy; 103 | vec2 chroma_k = CHROMA_tex(vec2((fp + vec2( 1.5, 1.5)) * CHROMA_pt)).xy; 104 | vec2 chroma_l = CHROMA_tex(vec2((fp + vec2( 2.5, 1.5)) * CHROMA_pt)).xy; 105 | vec2 chroma_n = CHROMA_tex(vec2((fp + vec2(0.5, 2.5) ) * CHROMA_pt)).xy; 106 | vec2 chroma_o = CHROMA_tex(vec2((fp + vec2(1.5, 2.5) ) * CHROMA_pt)).xy; 107 | 108 | float luma_0 = LUMA_texOff(0.0).x; 109 | float luma_b = LUMA_LOWRES_tex(vec2((fp + vec2(0.5, -0.5)) * CHROMA_pt)).x; 110 | float luma_c = LUMA_LOWRES_tex(vec2((fp + vec2(1.5, -0.5)) * CHROMA_pt)).x; 111 | float luma_e = LUMA_LOWRES_tex(vec2((fp + vec2(-0.5, 0.5)) * CHROMA_pt)).x; 112 | float luma_f = LUMA_LOWRES_tex(vec2((fp + vec2( 0.5, 0.5)) * CHROMA_pt)).x; 113 | float luma_g = LUMA_LOWRES_tex(vec2((fp + vec2( 1.5, 0.5)) * CHROMA_pt)).x; 114 | float luma_h = LUMA_LOWRES_tex(vec2((fp + vec2( 2.5, 0.5)) * CHROMA_pt)).x; 115 | float luma_i = LUMA_LOWRES_tex(vec2((fp + vec2(-0.5, 1.5)) * CHROMA_pt)).x; 116 | float luma_j = LUMA_LOWRES_tex(vec2((fp + vec2( 0.5, 1.5)) * CHROMA_pt)).x; 117 | float luma_k = LUMA_LOWRES_tex(vec2((fp + vec2( 1.5, 1.5)) * CHROMA_pt)).x; 118 | float luma_l = LUMA_LOWRES_tex(vec2((fp + vec2( 2.5, 1.5)) * CHROMA_pt)).x; 119 | float luma_n = LUMA_LOWRES_tex(vec2((fp + vec2(0.5, 2.5) ) * CHROMA_pt)).x; 120 | float luma_o = LUMA_LOWRES_tex(vec2((fp + vec2(1.5, 2.5) ) * CHROMA_pt)).x; 121 | 122 | float wd_b = comp_wd(vec2( 0.0,-1.0) - pp); 123 | float wd_c = comp_wd(vec2( 1.0,-1.0) - pp); 124 | float wd_e = comp_wd(vec2(-1.0, 0.0) - pp); 125 | float wd_f = comp_wd(vec2( 0.0, 0.0) - pp); 126 | float wd_g = comp_wd(vec2( 1.0, 0.0) - pp); 127 | float wd_h = comp_wd(vec2( 2.0, 0.0) - pp); 128 | float wd_i = comp_wd(vec2(-1.0, 1.0) - pp); 129 | float wd_j = comp_wd(vec2( 0.0, 1.0) - pp); 130 | float wd_k = comp_wd(vec2( 1.0, 1.0) - pp); 131 | float wd_l = comp_wd(vec2( 2.0, 1.0) - pp); 132 | float wd_n = comp_wd(vec2( 0.0, 2.0) - pp); 133 | float wd_o = comp_wd(vec2( 1.0, 2.0) - pp); 134 | 135 | float wi_b = comp_wi(luma_0 - luma_b); 136 | float wi_c = comp_wi(luma_0 - luma_c); 137 | float wi_e = comp_wi(luma_0 - luma_e); 138 | float wi_f = comp_wi(luma_0 - luma_f); 139 | float wi_g = comp_wi(luma_0 - luma_g); 140 | float wi_h = comp_wi(luma_0 - luma_h); 141 | float wi_i = comp_wi(luma_0 - luma_i); 142 | float wi_j = comp_wi(luma_0 - luma_j); 143 | float wi_k = comp_wi(luma_0 - luma_k); 144 | float wi_l = comp_wi(luma_0 - luma_l); 145 | float wi_n = comp_wi(luma_0 - luma_n); 146 | float wi_o = comp_wi(luma_0 - luma_o); 147 | 148 | float w_b = comp_w(wd_b, wi_b); 149 | float w_c = comp_w(wd_c, wi_c); 150 | float w_e = comp_w(wd_e, wi_e); 151 | float w_f = comp_w(wd_f, wi_f); 152 | float w_g = comp_w(wd_g, wi_g); 153 | float w_h = comp_w(wd_h, wi_h); 154 | float w_i = comp_w(wd_i, wi_i); 155 | float w_j = comp_w(wd_j, wi_j); 156 | float w_k = comp_w(wd_k, wi_k); 157 | float w_l = comp_w(wd_l, wi_l); 158 | float w_n = comp_w(wd_n, wi_n); 159 | float w_o = comp_w(wd_o, wi_o); 160 | 161 | float wt = 0.0; 162 | wt += w_b; 163 | wt += w_c; 164 | wt += w_e; 165 | wt += w_f; 166 | wt += w_g; 167 | wt += w_h; 168 | wt += w_i; 169 | wt += w_j; 170 | wt += w_k; 171 | wt += w_l; 172 | wt += w_n; 173 | wt += w_o; 174 | 175 | vec2 ct = vec2(0.0); 176 | ct += w_b * chroma_b; 177 | ct += w_c * chroma_c; 178 | ct += w_e * chroma_e; 179 | ct += w_f * chroma_f; 180 | ct += w_g * chroma_g; 181 | ct += w_h * chroma_h; 182 | ct += w_i * chroma_i; 183 | ct += w_j * chroma_j; 184 | ct += w_k * chroma_k; 185 | ct += w_l * chroma_l; 186 | ct += w_n * chroma_n; 187 | ct += w_o * chroma_o; 188 | 189 | vec4 output_pix = vec4(0.0, 0.0, 0.0, 1.0); 190 | output_pix.xy = ct / wt; 191 | output_pix.xy = clamp(output_pix.xy, 0.0, 1.0); 192 | return output_pix; 193 | } 194 | --------------------------------------------------------------------------------