lib/video.sh

8.0 KB · 333 lines · bash

1#!/usr/bin/env bash
2# Terminal video player — mpv only (tct/kitty vo) with progress bar.
3
4BRT_YTDLP_FORMAT="${BRT_YTDLP_FORMAT:-best[ext=mp4][vcodec^=avc1][height<=720]/best[ext=mp4][height<=720]/best[height<=720]/best[height<=480]}"
5
6brt_video_tty() {
7 if [[ -w /dev/tty ]]; then
8 printf '/dev/tty'
9 else
10 printf '/dev/stdout'
11 fi
12}
13
14brt_video_term_prepare() {
15 local tty rows
16 tty=$(brt_video_tty)
17 rows=$(tput lines 2>/dev/null || echo 24)
18 rows=$(( rows < 10 ? 24 : rows ))
19 {
20 # Alt-screen wheel → cursor keys, not scrollback (prevents tct corruption).
21 printf '\033[?1007h'
22 # Lock scroll region to the viewport so trailing newlines can't shift the canvas.
23 printf '\033[1;%dr' "$rows"
24 } >"$tty" 2>/dev/null || true
25}
26
27# Reinitialize the terminal after playback (mpv tct often skips cleanup on SIGINT).
28brt_video_term_restore() {
29 local tty vo="${BRT_VIDEO_VO:-}" rows cols
30 [[ -w /dev/tty ]] || return 0
31 tty=$(brt_video_tty)
32 {
33 [[ "$vo" == "kitty" ]] && printf '\033_Ga=d,a=A\033\\'
34 printf '\033[?1007l\033[r\033[?1049l\033[?47l'
35 } >"$tty" 2>/dev/null || true
36 unset COLUMNS LINES
37 if read -r rows cols _ < <(stty size 2>/dev/null <"$tty"); then
38 stty rows "$rows" cols "$cols" <"$tty" 2>/dev/null || true
39 fi
40 reset >"$tty" 2>&1 <"$tty" || stty sane <"$tty" 2>/dev/null || true
41 kill -WINCH $$ 2>/dev/null || true
42}
43
44brt_video_on_int() {
45 brt_video_term_restore
46}
47
48brt_video_on_exit() {
49 brt_video_term_restore
50 unset BRT_VIDEO_VO
51}
52
53brt_video_input_conf() {
54 local f
55 f=$(mktemp "${TMPDIR:-/tmp}/brt-mpv-input.XXXXXX")
56 cat > "$f" << 'EOF'
57WHEEL_UP ignore
58WHEEL_DOWN ignore
59WHEEL_LEFT ignore
60WHEEL_RIGHT ignore
61EOF
62 printf '%s' "$f"
63}
64
65brt_video_is_url() {
66 [[ "${1:-}" =~ ^https?:// ]]
67}
68
69brt_video_resolve_local_path() {
70 local p="$1" dir base
71 case "$p" in
72 ~/*) p="${HOME}/${p#~/}" ;;
73 ~) p="$HOME" ;;
74 esac
75 if [[ ! -f "$p" ]]; then
76 return 1
77 fi
78 if command -v realpath &>/dev/null; then
79 realpath "$p"
80 return 0
81 fi
82 dir=$(cd "$(dirname "$p")" 2>/dev/null && pwd) || return 1
83 base=$(basename "$p")
84 printf '%s/%s' "$dir" "$base"
85}
86
87brt_video_is_youtube() {
88 [[ "${1:-}" =~ (youtube\.com|youtu\.be|youtube-nocookie\.com) ]]
89}
90
91brt_video_ytdlp() {
92 if command -v yt-dlp &>/dev/null; then
93 printf '%s' "yt-dlp"
94 return 0
95 fi
96 if command -v youtube-dl &>/dev/null; then
97 printf '%s' "youtube-dl"
98 return 0
99 fi
100 return 1
101}
102
103brt_video_resolve_youtube() {
104 local url="$1" ytdlp stream
105 ytdlp=$(brt_video_ytdlp) || return 1
106 stream=$("$ytdlp" --no-playlist -f "${BRT_YTDLP_FORMAT}" -g "$url" 2>/dev/null | head -1)
107 if [[ -z "$stream" ]]; then
108 stream=$("$ytdlp" --no-playlist -f best -g "$url" 2>/dev/null | head -1)
109 fi
110 [[ -n "$stream" ]] || return 1
111 printf '%s' "$stream"
112}
113
114brt_video_vo_supported() {
115 local vo="$1" help
116 help=$(mpv --vo=help 2>/dev/null || true)
117 [[ -n "$help" ]] && grep -qE "(^|[[:space:]])${vo}([[:space:]]|$)" <<< "$help"
118}
119
120brt_video_pick_vo() {
121 local force="${BRT_VIDEO_VO_FORCE:-}" vo
122
123 if [[ -n "$force" ]] && brt_video_vo_supported "$force"; then
124 printf '%s' "$force"
125 return 0
126 fi
127
128 if [[ "${TERM_PROGRAM:-}" == *[Gg]hostty* ]] && brt_video_vo_supported tct; then
129 printf '%s' "tct"
130 return 0
131 fi
132
133 for vo in tct kitty; do
134 if brt_video_vo_supported "$vo"; then
135 printf '%s' "$vo"
136 return 0
137 fi
138 done
139 return 1
140}
141
142brt_video_tct_algo() {
143 local algo="${BRT_TCT_ALGO:-half-blocks}"
144 case "$algo" in
145 plain|half-blocks) printf '%s' "$algo" ;;
146 *) printf '%s' "half-blocks" ;;
147 esac
148}
149
150brt_video_size() {
151 local cols rows
152 cols=0
153 rows=0
154
155 if [[ -r /dev/tty ]]; then
156 read -r rows cols _ < <(stty size 2>/dev/null </dev/tty) || true
157 fi
158 if ! [[ "$cols" =~ ^[0-9]+$ && "$rows" =~ ^[0-9]+$ && "$cols" -gt 0 && "$rows" -gt 0 ]]; then
159 cols=$(tput cols 2>/dev/null || echo 80)
160 rows=$(tput lines 2>/dev/null || echo 24)
161 fi
162
163 cols=$(( cols < 10 ? 80 : cols ))
164 rows=$(( rows < 10 ? 24 : rows ))
165 printf '%s %s\n' "$cols" "$rows"
166}
167
168brt_video_user_ok() {
169 local rc="${1:-0}"
170 (( rc == 0 || rc == 130 || rc == 143 ))
171}
172
173brt_video_play() {
174 local source="$1"
175 local audio="$2"
176 local cols rows vo algo rc=0 log input_conf
177 local -a mpv_args=()
178 local -a network_args=()
179
180 read -r cols rows < <(brt_video_size)
181 vo=$(brt_video_pick_vo) || return 127
182 export BRT_VIDEO_VO="$vo"
183 algo=$(brt_video_tct_algo)
184 log=$(mktemp "${TMPDIR:-/tmp}/brt-mpv.XXXXXX")
185 input_conf=$(brt_video_input_conf)
186
187 brt_video_term_prepare
188
189 mpv_args=(
190 --quiet
191 --terminal=no
192 --input-terminal=yes
193 --input-conf="$input_conf"
194 --msg-level=all=no
195 --log-file="$log"
196 --force-window=no
197 --keep-open=no
198 --term-osd=force
199 --term-osd-bar=yes
200 --term-status-msg='${time-pos}/${duration} (${percent-pos}%)'
201 --vo="$vo"
202 --keepaspect=no
203 --panscan=1.0
204 --video-unscaled=no
205 --autosync=30
206 --video-sync=display-resample
207 --cache=yes
208 --cache-pause=no
209 --demuxer-readahead-secs=30
210 --cache-secs=90
211 --profile=sw-fast
212 )
213
214 case "$vo" in
215 tct)
216 mpv_args+=(
217 --vo-tct-width="$cols"
218 --vo-tct-height="$rows"
219 --vo-tct-algo="$algo"
220 --vo-tct-buffering=frame
221 --monitorpixelaspect=0.5
222 )
223 ;;
224 kitty)
225 mpv_args+=(
226 --vo-kitty-alt-screen=yes
227 --vo-kitty-config-clear=yes
228 --vo-kitty-cols="$cols"
229 --vo-kitty-rows="$rows"
230 )
231 ;;
232 esac
233
234 if brt_video_is_youtube "$source"; then
235 local stream
236 if stream=$(brt_video_resolve_youtube "$source"); then
237 source="$stream"
238 network_args=(
239 --network-timeout=120
240 --stream-lavf-o=reconnect=1,reconnect_streamed=1,reconnect_delay_max=5,fflags=+discardcorrupt
241 )
242 else
243 network_args=(--ytdl=yes --ytdl-format="${BRT_YTDLP_FORMAT}" --network-timeout=120)
244 fi
245 elif brt_video_is_url "$source"; then
246 network_args=(
247 --network-timeout=60
248 --stream-lavf-o=reconnect=1,reconnect_streamed=1,reconnect_delay_max=5,fflags=+discardcorrupt
249 )
250 else
251 local resolved
252 if ! resolved=$(brt_video_resolve_local_path "$source"); then
253 brt_error "Not found: ${source}"
254 rm -f "$log" "$input_conf"
255 return 1
256 fi
257 source="$resolved"
258 network_args=(--no-ytdl)
259 fi
260
261 if ((${#network_args[@]} > 0)); then
262 mpv_args+=("${network_args[@]}")
263 fi
264 (( audio )) || mpv_args+=(--no-audio)
265
266 # Foreground mpv keeps keyboard input; restore runs on INT trap and again after exit.
267 mpv "${mpv_args[@]}" "$source" 2>/dev/null
268 rc=$?
269 if ! brt_video_user_ok "$rc" && [[ -s "$log" ]]; then
270 tail -5 "$log" >&2
271 fi
272 rm -f "$log" "$input_conf"
273 brt_video_term_restore
274 return "$rc"
275}
276
277brt_video() {
278 local source="${1:-}"
279 local audio=1 rc
280
281 if [[ -z "$source" ]]; then
282 brt_error "Usage: brt -video <file|url> [--no-audio]"
283 brt_info "Terminal video via mpv — space pause, q quit, arrows seek"
284 brt_info "Press q to quit — terminal restores automatically on exit"
285 brt_info "Supports local files, HTTP URLs, and YouTube links"
286 exit 1
287 fi
288
289 shift
290 while [[ $# -gt 0 ]]; do
291 case "$1" in
292 --no-audio) audio=0; shift ;;
293 -x|-y|-w|--width|-fps)
294 brt_warn "Deprecated option ignored: $1 (video fills the terminal)"
295 shift 2
296 ;;
297 --tiny-font|--no-tiny-font)
298 brt_warn "Deprecated option ignored: $1"
299 shift
300 ;;
301 *)
302 brt_error "Unknown option: $1"
303 brt_info "Usage: brt -video <file|url> [--no-audio]"
304 exit 1
305 ;;
306 esac
307 done
308
309 command -v mpv &>/dev/null || {
310 brt_error "mpv is required for video playback"
311 brt_info "Install: brew install mpv"
312 exit 1
313 }
314
315 if brt_video_is_youtube "$source" && ! brt_video_ytdlp &>/dev/null; then
316 brt_warn "Install yt-dlp for reliable YouTube: brew install yt-dlp"
317 fi
318
319 trap 'brt_video_on_int' INT
320 trap 'brt_video_on_exit' EXIT
321
322 set +e
323 brt_video_play "$source" "$audio"
324 rc=$?
325 set -e
326
327 if brt_video_user_ok "$rc"; then
328 return 0
329 fi
330 brt_error "Playback failed (exit ${rc})"
331 return "$rc"
332}
333