My scripts for using ffmpeg to transcode recordings and fiddle the mythtv database.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

616 lines
32 KiB

  1. #!/usr/bin/guile \
  2. -e ffmpeg-myth -s
  3. !#
  4. ;;-*- geiser-scheme-implementation: guile -*-
  5. ;;-*- coding: utf-8 -*-
  6. (use-modules (ice-9 rdelim) (ice-9 regex) (ice-9 popen))
  7. (use-modules (srfi srfi-1))
  8. (use-modules (srfi srfi-9 gnu))
  9. (use-modules ((rnrs) :version (6)))
  10. ;;(use-modules ((guile json)))
  11. (define mysql-cnf "~/my.cnf")
  12. (define mysql-bin "mysql")
  13. (define mythtv-db "mythconverg")
  14. (define mysql-row-sep #\newline)
  15. (define mysql-col-sep #\tab)
  16. (define ffmpeg-bin "/usr/bin/ffmpeg")
  17. (define ffprobe-bin "/usr/bin/ffprobe")
  18. (define ffprobe-separator #\page) ;;
  19. (define ffmpeg-transcoding-options "-c:v libx264 -preset slow -crf 21 -c:a ac3 -g 60 -keyint_min 30")
  20. (define ffmpeg-transcoded-file-muxer 'mpegts) ;; 'matroska
  21. (define recordings-directory "/var/lib/mythtv/recordings")
  22. (define working-directory "/mnt/lvraid5/ffmpeg-cut-list.d")
  23. (define mythtv-recorded-table->metadata-list '(chanid starttime endtime title subtitle description season episode recordid seriesid programid inetref previouslyshown originalairdate))
  24. (define (mythconverg-input-file-name)
  25. (string-copy (string-join (list working-directory "ffmpeg-mythconverg.XXXXXX") "/")))
  26. (define shell-command-log #t)
  27. (define (roundx x) (inexact->exact (round x)))
  28. (define (write-to-file obj filename)
  29. (let ((port (open-file filename "w")))
  30. (catch 'write-error
  31. ;; thunk
  32. (lambda () (simple-format port "~a" obj) (force-output) (close port))
  33. ;; handler
  34. (lambda () (close port) 'write-error))))
  35. (define* (shell-command-to-string* cmd args)
  36. (let* ((port (apply open-pipe* (append (list OPEN_READ cmd) args)))
  37. (str (read-string port)))
  38. (close port)
  39. str))
  40. (define* (shell-command-to-string cmd)
  41. (cond (shell-command-log
  42. (simple-format #t "shell-command-to-string: ~a\n" cmd) (force-output)))
  43. (catch 'shell-command-error
  44. ;; thunk
  45. (lambda ()
  46. (let* ((port (open-pipe cmd OPEN_READ))
  47. (str (read-string port))
  48. (wtpd (close-pipe port))
  49. (xval (status:exit-val wtpd)))
  50. (if (or (eqv? xval #f) (> xval 0)) (throw 'shell-command-error cmd str))
  51. (cond (shell-command-log
  52. (simple-format #t "shell-command-to-string result: ~a\n" str) (force-output)))
  53. str))
  54. ;; handler
  55. (lambda (key cmd str)
  56. (simple-format #t "ERROR: in command ~a\nstring: ~a\n" cmd str)
  57. (throw 'error-in-shell-command-to-string cmd str))))
  58. (define (mysql-escape-quote str)
  59. ;; (string-join (string-split str #\') "'\"'\"'"))
  60. (string-join (string-split str #\') "\""))
  61. (define (mythconverg-execute0 statement)
  62. (shell-command-to-string (simple-format #f "~a --defaults-file=~a --database=~a --execute='~a'" mysql-bin mysql-cnf mythtv-db (mysql-escape-quote statement))))
  63. (define (mythconverg-source file)
  64. (mythconverg-execute0 (simple-format #f "source ~a" file)))
  65. (define (mythconverg-execute statement)
  66. (cond ((< (string-length statement) 1024) ;; really could use < ARG_MAX (see getconf ARG_MAX)
  67. (mythconverg-execute0 statement))
  68. (#t
  69. (let* ((port (mkstemp! (mythconverg-input-file-name)))
  70. (tmpfile (port-filename port)))
  71. (simple-format port statement) (force-output port) (close-port port)
  72. (mythconverg-source tmpfile)
  73. tmpfile))))
  74. (define mysql-cmd "")
  75. (define (mysql-start-transaction cmd)
  76. (set! mysql-cmd (simple-format #f "start transaction; ~a" cmd)))
  77. (define (mysql-continue cmd)
  78. (set! mysql-cmd (simple-format #f "~a ~a" mysql-cmd cmd)))
  79. (define (mysql-commit cmd)
  80. (set! mysql-cmd (simple-format #f "~a ~a commit;" mysql-cmd cmd)))
  81. (define (elt n l)
  82. (let ((n* (if (>= n 0) n (+ (length l) n))))
  83. (car (drop l n*))))
  84. (define (read-from-string s)
  85. (cond ((string? s)
  86. (let ((r (with-input-from-string s read)))
  87. (cond ((eof-object? r) s)
  88. ((not (symbol? r)) r)
  89. ((string= s (simple-format #f "~a" r))
  90. r)
  91. (#t s))))
  92. ((list? s)
  93. (map read-from-string s))
  94. (#t
  95. (error "read-from-string" "encountered a not list/string"))))
  96. (define (dsv-splitter s d)
  97. (cond ((string? s)
  98. (string-split s d))
  99. ((list? s)
  100. (map (lambda(x) (dsv-splitter x d)) s))
  101. (#t
  102. (error "dsv-splitter" "encountered a not list/string"))))
  103. (define* (ffprobe-out-parser s)
  104. (dsv-splitter (delete "" (dsv-splitter s #\newline)) ffprobe-separator))
  105. (define (k-v-parser s)
  106. (read-from-string (dsv-splitter s #\=)))
  107. (define (map-cons l)
  108. (apply (lambda(x y) (map cons x y)) l))
  109. (define (make-dotted-alist s)
  110. (cond ((and (list? s) (= 2 (length s)) (symbol? (car s)))
  111. (cons (car s) (cadr s)))
  112. ((and (list? s) (> (length s) 2))
  113. (map make-dotted-alist s))
  114. (#t s)))
  115. (define (mysql-parse-row r)
  116. (map read-from-string (string-split r mysql-col-sep)))
  117. (define (mysql-parse-output s)
  118. (map mysql-parse-row (drop-right (string-split s mysql-row-sep) 1)))
  119. (define (mythconverg-execute+parse statement)
  120. (mysql-parse-output (mythconverg-execute statement)))
  121. (define (mythconverg-select* table chanid starttime)
  122. (mythconverg-execute+parse (simple-format #f "select * from ~a where chanid=~a and starttime=~a;" table chanid starttime)))
  123. (define (mythconverg-get-recorded chanid starttime)
  124. (map-cons (mythconverg-select* "recorded" chanid starttime)))
  125. (define (mythconverg-get-recordedmarkup chanid starttime)
  126. (map (lambda(x) (drop x 2)) (mythconverg-select* "recordedmarkup" chanid starttime)))
  127. (define (ffprobe-video-packets file filter-rule)
  128. (let ((frame-info (read-from-string (ffprobe-out-parser
  129. (shell-command-to-string (simple-format #f "~a -v error -select_streams v:0 -show_entries frame=pkt_pts_time,pkt_pos,pict_type,coded_picture_number,interlaced_frame:side_data=nil -print_format csv=nokey=1:print_section=0:s='~a' -i '~a'" ffprobe-bin ffprobe-separator file)))))
  130. (c -1))
  131. (define (add-coded_picture_number l)
  132. (set! c (+ c 1))
  133. (list (first l) (second l) (third l) c (fifth l)))
  134. (filter filter-rule (map add-coded_picture_number frame-info))))
  135. (define (ffprobe-i-frames file)
  136. ;; filter out bad I-frames with N/A data in either the time (1) or frame number slot (2)
  137. (ffprobe-video-packets file (lambda(l) (and (eq? 'I (third l)) (not (eq? 'N/A (first l))) (not (eq? 'N/A (second l)))))))
  138. (define (ffprobe-stream-info file)
  139. (let* ((stream-info (shell-command-to-string (simple-format #f "~a -v error -show_streams -print_format csv=nokey=0:s='~a' -i '~a'" ffprobe-bin ffprobe-separator file))))
  140. (map make-dotted-alist (k-v-parser (ffprobe-out-parser stream-info)))))
  141. (define (mysql-get-cut/skip-list chanid starttime cut-or-skip)
  142. (let* ((s-e (if (eq? cut-or-skip 'cut) '(1 . 0) '(4 . 5)))
  143. (s (mythconverg-execute+parse (simple-format #f "select mark,type from recordedmarkup where chanid=~a and starttime=~a and (type=~a or type=~a) order by mark;" chanid starttime (car s-e) (cdr s-e))))
  144. (f (mythconverg-execute+parse (simple-format #f "select data from recordedmarkup where chanid=~a and starttime=~a and type=34;" chanid starttime))))
  145. (list (if (null? s) s (cdr s)) (if (null? f) f (cdr f)))))
  146. (define (mythtv-make-concat-list s f)
  147. (let* ((mark first) (type second)
  148. (start-of-cut? (lambda(x) (or (eq? x 1) (eq? x 4))))
  149. (end-of-cut? (lambda(x) (or (eq? x 0) (eq? x 5))))
  150. (start 1) (end 0))
  151. (define (cl l acc)
  152. (if (null? l) (reverse acc)
  153. (cond ((end-of-cut? (cdar l))
  154. (cl (cddr l) (if (< (caar l) (caadr l)) (append (list (cons (caar l) (caadr l))) acc) acc)))
  155. (#t
  156. (cl (cdr l) acc)))))
  157. (cond ((null? s)
  158. (set! s '((0 0))))
  159. ((and (start-of-cut? (cadar s)) (> (caar s) 0))
  160. (set! s (append '((0 0)) s))))
  161. (if (end-of-cut? (cadr (last s)))
  162. (set! s (append s (list (list (if (null? f) (greatest-fixnum) (caar f)) 1)))))
  163. (set! s (map (lambda(x) (if (start-of-cut? (cadr x)) (cons (car x) start) (cons (car x) end)))
  164. s))
  165. (cl s '())))
  166. (define (mythtv-get-retention-list chanid starttime cut-or-skip)
  167. (apply mythtv-make-concat-list (mysql-get-cut/skip-list chanid starttime cut-or-skip)))
  168. (define (mythtv-find-nearest-i-frame-before/after idx frame-info test i-frame)
  169. (cond ((null? frame-info) i-frame)
  170. (#t
  171. (let* ((i-frame (car frame-info))
  172. (cpn (fourth i-frame)))
  173. (if (test cpn idx) i-frame (mythtv-find-nearest-i-frame-before/after idx (cdr frame-info) test i-frame))))))
  174. (define (mythtv-find-nearest-i-frame-before idx frame-info)
  175. (mythtv-find-nearest-i-frame-before/after idx frame-info >= (first frame-info)))
  176. (define (mythtv-find-nearest-i-frame-after idx frame-info)
  177. (mythtv-find-nearest-i-frame-before/after idx (reverse frame-info) <= (last frame-info)))
  178. (define-immutable-record-type recording
  179. (make-recording chanid starttime recorded-table streams-info i-frame-info retain-list)
  180. recording?
  181. (chanid recording-chanid set-recording-chanid!)
  182. (starttime recording-starttime set-recording-starttime!)
  183. (recorded-table recording-recorded-table set-recording-recorded-table!)
  184. (streams-info recording-streams-info set-recording-streams-info!)
  185. (i-frame-info recording-i-frame-info set-recording-i-frame-info!)
  186. (retain-list recording-retain-list set-recording-retain-list!))
  187. ;; make a readable printer for the recording record
  188. (define* (recording-printer rec #:optional (port #t))
  189. (define (rec-printer fg)
  190. (let ((field (car fg)) (getter (cdr fg)))
  191. (simple-format port "(~s . ~s)\n" field (getter rec))))
  192. (let ((fields+getters (list
  193. (cons 'chanid recording-chanid)
  194. (cons 'starttime recording-starttime)
  195. (cons 'recorded-table recording-recorded-table)
  196. (cons 'streams-info recording-streams-info)
  197. (cons 'i-frame-info recording-i-frame-info)
  198. (cons 'retain-list recording-retain-list))))
  199. (simple-format port "'(")
  200. (map rec-printer fields+getters)
  201. (simple-format port ")\n")))
  202. (set-record-type-printer! recording recording-printer)
  203. (define (create-record-data chanid starttime cut-or-skip)
  204. (let* ((rec-tbl (mythconverg-get-recorded chanid starttime))
  205. (recmkp-tbl (mythconverg-get-recordedmarkup chanid starttime))
  206. (input-file (simple-format #f "~a/~a" recordings-directory (assv-ref rec-tbl 'basename)))
  207. (strms-inf (ffprobe-stream-info input-file))
  208. (i-frm-nfo (ffprobe-i-frames input-file))
  209. (rtn-lst (mythtv-get-retention-list chanid starttime cut-or-skip)))
  210. (make-recording chanid starttime rec-tbl strms-inf i-frm-nfo rtn-lst)))
  211. (define (re-create-record-data r-d)
  212. (if (recording? r-d) r-d
  213. (let ((lookup (lambda (key) (assv-ref r-d key))))
  214. (apply make-recording (map lookup '(chanid starttime recorded-table streams-info i-frame-info retain-list))))))
  215. (define recording-file-endings3 '("mpg" "mp4" "mkv" "h264-ts" "h264.ts" "h265.ts"))
  216. (define recording-file-endings2 '("ts"))
  217. (define transcoded-file-ending "h264.ts") ;; mkv
  218. (define remuxed-ts-file-ending "h264.ts")
  219. (define (recording-basename rec)
  220. (assv-ref (recording-recorded-table rec) 'basename))
  221. (define (recording-filesize rec)
  222. (assv-ref (recording-recorded-table rec) 'filesize))
  223. (define (recording-av-stream-info rec type)
  224. (define (rvsi s r) (cond ((null? s) r)
  225. ((eqv? (assv-ref (car s) 'codec_type) type)
  226. (rvsi (cdr s) (cons (car s) r)))
  227. (#t (rvsi (cdr s) r))))
  228. (rvsi (recording-streams-info rec) '()))
  229. (define (recording-video-stream-ffmpeg-filt rec scale?)
  230. (define (iter f l i acc)
  231. (cond ((null? l) (reverse acc))
  232. (#t
  233. (iter f (cdr l) (+ 1 i) (cons (f i) acc)))))
  234. (let* ((v (car (recording-av-stream-info rec 'video)))
  235. (a (reverse (recording-av-stream-info rec 'audio)))
  236. (rtn (recording-retain-list rec))
  237. ;;(sar (assv-ref v 'sample_aspect_ratio))
  238. (dar (assv-ref v 'display_aspect_ratio))
  239. (height (assv-ref v 'height))
  240. (width (assv-ref v 'width))
  241. (idx (assv-ref v 'index))
  242. (aidx (map (lambda(s) (assv-ref s 'index)) a))
  243. (scale-flt (if scale? (let ((fmt (simple-format #f "[~~a:~a] scale=w=~a:h=~a,setdar=~a,setpts=PTS-STARTPTS [v~~a];" idx width height dar)))
  244. (lambda (i) (simple-format #f fmt i i))) (lambda (i) "")))
  245. (av-ptr (lambda (i) (string-join (cons (if scale? (simple-format #f "[v~a]" i) (simple-format #f "[~a:~a]" idx i)) (map (lambda (a) (simple-format #f "[~a:~a]" i a)) aidx)))))
  246. (out-ptr (lambda (i) (simple-format #f "[out~a]" i)))
  247. (n (length rtn))
  248. (concat (simple-format #f "concat=n=~a:v=1:a=~a [out0]" n (length aidx)))
  249. (mappings (lambda (i) (simple-format #f "-map '[out~a]'" i))))
  250. (string-join (append (list "'") (iter scale-flt rtn 0 '()) (iter av-ptr rtn 0 '()) (list concat) (iter out-ptr aidx 1 '()) (list "' ") (iter mappings (cons 0 aidx) 0 '())))))
  251. (define* (ffmpeg-split+scale+transcode-recording rec #:optional (scale? #f))
  252. (let* ((basename (recording-basename rec))
  253. (rootname+ext (string-split basename #\.))
  254. (rootname (car rootname+ext))
  255. (in-file (simple-format #f "~a/~a" recordings-directory basename))
  256. (out-file (simple-format #f "~a/~a.~a" working-directory rootname transcoded-file-ending))
  257. (m-d (mythtv-recorded-table->metadata rec))
  258. (cut-times (myth-convert-i-frame-indices-to-times (recording-retain-list rec) (recording-i-frame-info rec)))
  259. (seek-fn (lambda(c) (simple-format #f " -ss ~a -t ~a -i '~a'" (car c) (- (cdr c) (car c)) in-file)))
  260. (filter (recording-video-stream-ffmpeg-filt rec scale?))
  261. (options ffmpeg-transcoding-options)
  262. (cmd (simple-format #f "~a -y ~a -filter_complex ~a ~a ~a ~a" ffmpeg-bin (string-join (map seek-fn cut-times)) filter options m-d out-file)))
  263. (shell-command-to-string cmd)
  264. cmd))
  265. (define (recording-new-file-name file-name ending)
  266. (let* ((base+exts (string-split file-name #\.))
  267. (base (car base+exts)))
  268. (simple-format #f "~a.~a" base ending)))
  269. (define* (create-record-data-for-transcode rec)
  270. (let* ((rtn-list '())
  271. (tr-rec rec)
  272. (rec-tbl (copy-tree (recording-recorded-table tr-rec)))
  273. (basename (recording-basename tr-rec))
  274. (tr-basename (recording-new-file-name basename transcoded-file-ending))
  275. (input-file (simple-format #f "~a/~a" recordings-directory basename))
  276. (input-tr-file (simple-format #f "~a/~a" working-directory tr-basename))
  277. (tr-filesize (stat:size (stat input-tr-file)))
  278. (strms-inf (ffprobe-stream-info input-tr-file))
  279. (i-frm-nfo (ffprobe-i-frames input-tr-file)))
  280. (set! rec-tbl (assv-set! rec-tbl 'basename tr-basename))
  281. (set! rec-tbl (assv-set! rec-tbl 'filesize tr-filesize))
  282. (set! rec-tbl (assv-set! rec-tbl 'cutlist 0))
  283. (set! rec-tbl (assv-set! rec-tbl 'commflagged 0))
  284. (set! rec-tbl (assv-set! rec-tbl 'bookmark 0))
  285. (set! rec-tbl (assv-set! rec-tbl 'transcoded 1))
  286. (set-fields tr-rec ((recording-recorded-table) rec-tbl)
  287. ((recording-streams-info) strms-inf)
  288. ((recording-i-frame-info) i-frm-nfo)
  289. ((recording-retain-list) rtn-list))))
  290. ;; (define (ffmpeg-split+retain-recording rec)
  291. ;; (let* ((basename (recording-basename rec))
  292. ;; (rootname+ext (string-split basename #\.))
  293. ;; (rootname (car rootname+ext)) (ext (cadr rootname+ext))
  294. ;; (in-file (simple-format #f "~a/~a" recordings-directory basename))
  295. ;; (out-file (simple-format #f "~a/~a.mkv" working-directory rootname))
  296. ;; (out-ffconcat (simple-format #f "~a/~a.ffconcat" working-directory rootname))
  297. ;; (frames-list (string-join (map (lambda(x)(simple-format #f "~a,~a" (car x)(cdr x))) (recording-retain-list rec)) "," 'infix))
  298. ;; (out-file-tmp (simple-format #f "~a/~a_%05d.~a" working-directory rootname ext))
  299. ;; (ffmpeg-cut (simple-format #f "ffmpeg -fflags '+genpts' -ignore_unknown -y -i \"~a\" -codec copy -map 0 -map -0:d -f segment -segment_list \"~a\" -segment_frames \"~a\" \"~a\"" in-file out-ffconcat frames-list out-file-tmp)))
  300. ;; (shell-command-to-string ffmpeg-cut)))
  301. (define (myth-convert-i-frame-indices-to-times indices i-frame-info)
  302. ;; index = ((a . b) ... )
  303. (let* ((start_time (caar i-frame-info))
  304. (normalize-times (lambda (l)
  305. (cons (- (car l) start_time) (- (cdr l) start_time)))))
  306. (define (m-c-i-f-i-t-t indices times)
  307. (cond ((null? indices) times)
  308. (#t
  309. (m-c-i-f-i-t-t (cdr indices)
  310. (append times
  311. (list (cons (car (mythtv-find-nearest-i-frame-after (caar indices) i-frame-info)) (car (mythtv-find-nearest-i-frame-before (cdar indices) i-frame-info)))))))))
  312. (map normalize-times (m-c-i-f-i-t-t indices '()))))
  313. (define (mythtv-recorded-table->metadata rec)
  314. (let* ((r-t (recording-recorded-table rec))
  315. (m-d (map (lambda (x) (assv x r-t)) mythtv-recorded-table->metadata-list))
  316. (sanitize (lambda (s) (if (string? s) (string-delete (lambda(c) (or (eqv? c #\") (eqv? c #\'))) s) s))))
  317. (string-join (map (lambda (x) (simple-format #f "-metadata \"~a=~a\"" (car x) (sanitize (cdr x)))) m-d) " ")))
  318. (define* (ffmpeg-filter-streams streams-info #:optional (max 2) (filter-lst '(video audio)))
  319. (let* ((codec-type (lambda (x) (assv-ref x 'codec_type) ))
  320. (filter-fn (lambda(x) (memq (codec-type x) filter-lst))))
  321. (define (sorter l)
  322. (stable-sort l (lambda (a b) (string> (symbol->string (codec-type a)) (symbol->string (codec-type b))))))
  323. (define (selector l max acc)
  324. (cond ((or (= max 0) (null? l)) acc) (#t (selector (cdr l) (- max 1) (append acc (list (car l)))))))
  325. (selector (sorter (filter filter-fn streams-info)) max '())))
  326. (define (ffmpeg-outstreams-templates rec max-streams)
  327. (let* ((streams-info (ffmpeg-filter-streams (recording-streams-info rec) max-streams))
  328. (s-i (map (lambda (x) (cons (substring (symbol->string (assv-ref x 'codec_type)) 0 1) (assv-ref x 'index))) streams-info))
  329. (concat-template (map (lambda(s) (simple-format #f "[~~a:~a]" (cdr s))) s-i))
  330. (i -1) (j -1)
  331. (output-template (map (lambda(x) (set! j (+ 1 j)) (simple-format #f "[out~a]" j)) streams-info)))
  332. (cons
  333. (lambda(x) (set! i (+ 1 i)) (string-join (map (lambda(f) (simple-format #f f i)) concat-template) " "))
  334. output-template)))
  335. (define* (ffmpeg-split+transcode-recording rec #:optional (max-streams 4)
  336. (transcoded-file-ending transcoded-file-ending)
  337. (ffmpeg-transcoded-file-muxer ffmpeg-transcoded-file-muxer))
  338. (let* ((basename (recording-basename rec))
  339. (rootname+ext (string-split basename #\.))
  340. (rootname (car rootname+ext))
  341. (in-file (simple-format #f "~a/~a" recordings-directory basename))
  342. (out-file (simple-format #f "~a/~a.~a" working-directory rootname transcoded-file-ending))
  343. (m-d (if (eq? ffmpeg-transcoded-file-muxer 'matroska) (mythtv-recorded-table->metadata rec) ""))
  344. (cut-times (myth-convert-i-frame-indices-to-times (recording-retain-list rec) (recording-i-frame-info rec)))
  345. (seek-fn (lambda(c) (simple-format #f " -ss ~a -t ~a -i '~a'" (car c) (- (cdr c) (car c)) in-file)))
  346. (concat-info (ffmpeg-outstreams-templates rec max-streams))
  347. (instreams (string-join (map (car concat-info) cut-times) " "))
  348. (outstreams (cdr concat-info))
  349. (mappings (map (lambda (x) (simple-format #f " -map '~a'" x)) outstreams))
  350. (cmd (simple-format #f "~a -y ~a -filter_complex '~a concat=n=~a:v=1:a=~a ~a' ~a ~a ~a -f ~a ~a" ffmpeg-bin (string-join (map seek-fn cut-times)) instreams (length cut-times) (- (length outstreams) 1) (string-join outstreams) ffmpeg-transcoding-options (string-join mappings) m-d ffmpeg-transcoded-file-muxer out-file)))
  351. (shell-command-to-string cmd)
  352. cmd))
  353. (define (mythconverg-update-recorded-seek-table rec)
  354. (let* ((i-frm-nfo (recording-i-frame-info rec))
  355. (start_pts (caar i-frm-nfo))
  356. (chanid (recording-chanid rec))
  357. (starttime (recording-starttime rec))
  358. (start-time (assv-ref (recording-recorded-table rec) 'starttime)))
  359. (define (type-9/33-marks l acc)
  360. (cond ((null? l) acc)
  361. ;; for some reason, ffmpeg's i-frame # is 2 less than that mythtv reports
  362. ;; after frame 0
  363. ;; (i-frame offset time)
  364. ((not (number? (first (car l))))
  365. (type-9/33-marks (cdr l) acc))
  366. ((null? acc)
  367. (type-9/33-marks (cdr l) (list (list (fourth (car l)) (second (car l)) (inexact->exact (round (* (- (first (car l)) start_pts) 1000)))))))
  368. (#t
  369. (type-9/33-marks (cdr l) (append acc (list (list (+ 2 (fourth (car l))) (second (car l)) (inexact->exact (round (* (- (first (car l)) start_pts) 1000))))))))))
  370. (define (printer* tpe x)
  371. (simple-format #f "insert into recordedseek (chanid,starttime,mark,offset,type) values (~a,~a,~a,~a,~a);\n" chanid starttime (car x) (if (= tpe 9) (cadr x) (caddr x)) tpe))
  372. (define (printer9 x) (printer* 9 x))
  373. (define (printer33 x) (printer* 33 x))
  374. (let* ((marks (type-9/33-marks i-frm-nfo '()))
  375. (delete-recseek (simple-format #f "delete from recordedseek where chanid=~a and starttime=~a and (type=9 or type=33);\n" chanid starttime))
  376. (insert-9 (string-join (map printer9 marks)))
  377. (insert-33 (string-join (map printer33 marks))))
  378. (mysql-continue (string-join (list delete-recseek insert-9 insert-33))))))
  379. (define (HH:MM:SS.x->milliseconds t)
  380. (let ((tm (read-from-string (string-split t #\:))))
  381. (roundx (* 1000 (+ (* 3600 (first tm)) (* 60 (second tm)) (third tm))))))
  382. (define (mythconverg-recordedmarkup-type34 rec)
  383. (fourth (last (recording-i-frame-info rec))))
  384. (define (mythconverg-recordedmarkup-type33 rec)
  385. (let* ((vid (car (filter (lambda(l) (eq? (assq-ref l 'codec_type) 'video)) (recording-streams-info rec))))
  386. (dur0 (assq-ref vid 'duration))
  387. (dur1 (assq-ref vid 'tag:DURATION)))
  388. (cond ((number? dur0) (roundx (* 1000 dur0)))
  389. ((string? dur1) (HH:MM:SS.x->milliseconds dur1))
  390. (#t 0))))
  391. (define* (mythconverg-update-recordedmarkup-table rec #:optional (continuation mysql-continue))
  392. (let* ((chanid (recording-chanid rec))
  393. (starttime (recording-starttime rec))
  394. (last-i-frame (mythconverg-recordedmarkup-type34 rec))
  395. (duration (mythconverg-recordedmarkup-type33 rec)))
  396. (continuation (simple-format #f "delete from ltbrecordedmarkup where chanid=~a and starttime=~a; insert into ltbrecordedmarkup (chanid,starttime,mark,type,data) select chanid,starttime,mark,type,data from recordedmarkup where chanid=~a and starttime=~a and type>=0; delete from recordedmarkup where chanid=~a and starttime=~a and (type<=5 or type>=33); insert into recordedmarkup (chanid,starttime,mark,type,data) values (~a,~a,0,34,~a); insert into recordedmarkup (chanid,starttime,mark,type,data) values (~a,~a,0,33,~a);" chanid starttime chanid starttime chanid starttime chanid starttime last-i-frame chanid starttime duration))))
  397. ;; should be identical to above, except the tables are reversed
  398. (define (mythconverg-update-recordedmarkup-table-rollback rec)
  399. (let ((sanitize+continue (lambda (s)
  400. (mysql-continue (string-downcase
  401. (regexp-substitute/global #f " recordedmarkup"
  402. (regexp-substitute/global #f " ltbrecordedmarkup" s 'pre " RECORDEDMARKUP" 'post) 'pre " LTBRECORDEDMARKUP" 'post))))))
  403. (mythconverg-update-recordedmarkup-table rec sanitize+continue)))
  404. (define* (mythconverg-update-recorded-table rec tr-rec #:optional (mv? #t))
  405. (let* ((chanid (recording-chanid rec))
  406. (starttime (recording-starttime rec))
  407. (basename (recording-basename rec))
  408. (tr-basename (recording-basename tr-rec))
  409. (input-file (simple-format #f "~a/~a" recordings-directory basename))
  410. (input-tr-file (simple-format #f "~a/~a" working-directory tr-basename))
  411. (tr-filesize (assv-ref (recording-recorded-table tr-rec) 'filesize)))
  412. ;; change timestamp on transcoded file to original timestamp and move it to recordings directory
  413. (if mv? (shell-command-to-string (simple-format #f "touch --reference='~a' '~a' && mv '~a' '~a'" input-file input-tr-file input-tr-file recordings-directory)))
  414. (mysql-commit (simple-format #f "update recorded set cutlist=0, commflagged=0, bookmark=0, transcoded=1, filesize=~a, basename=\"~a\" where chanid=~a and starttime=~a;" tr-filesize tr-basename chanid starttime))))
  415. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  416. ;;;; MYTH-CLEAN-RECORDINGS
  417. ;;;; - on successful transcode, FFMPEG-MYTH0 calls MYTHCONVERG-UPDATE-LTBTRANSCODEDTOBEDELETED
  418. ;;;; which adds an entry in the sql table ltbtranscodedtobedeleted for the file that was transcoded.
  419. ;;;; - MYTH-CLEAN-RECORDING-DIRECTORY queries ltbtranscodedtobedeleted for the list of files with an
  420. ;;;; expiry date before now+n days; it executes SHELL-CMD on that list; if there are no expired files
  421. ;;;; it returns an empty list, otherwise the list of files
  422. ;;;; - MYTHCONVERG-UPDATE-DELETED-LTBTRANSCODEDTOBEDELETED takes the list of files from
  423. ;;;; MYTH-CLEAN-RECORDING-DIRECTORY and sets the deleted flag on each one in the the ltbtranscodedtobedeleted
  424. ;;;; table.
  425. ;;;;
  426. ;;;; Debug/test: shell-cmd <- echo ; continuation <- (lambda(x) x)
  427. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  428. (define* (mythconverg-update-ltbtranscodedtobedeleted rec #:optional (continuation mysql-continue))
  429. (let* ((basename (if (recording? rec) (recording-basename rec) rec)))
  430. (continuation (simple-format #f "insert into ltbtranscodedtobedeleted (basename,deleted,expirydate) values ('~a',0,NOW() + interval 14 day);" basename))))
  431. (define* (myth-clean-recording-directory #:optional (expiration-date 1) (shell-cmd "rm -f"))
  432. (let* ((safe-cdr (lambda (x) (if (null? x) x (cdr x))))
  433. (expired-recordings (map car (safe-cdr (mythconverg-execute+parse (simple-format #f "select basename from ltbtranscodedtobedeleted where deleted=0 and expirydate <= NOW() + interval ~a day;" expiration-date))))))
  434. (cond ((null? expired-recordings)
  435. (simple-format #t "myth-clean-recording-directory: no transcoded recordings to remove. Done.\n")
  436. '())
  437. (#t
  438. (simple-format #t "myth-clean-recording-directory: removing transcoded files\n~a\n" (string-join expired-recordings ","))
  439. (let ((cmd (simple-format #f "~a ~a" shell-cmd (string-join (map (lambda(s) (string-concatenate (list recordings-directory "/" s "*"))) expired-recordings) " "))))
  440. (catch 'error-in-shell-command-to-string
  441. ;; thunk - return expired-recordings list
  442. (lambda ()
  443. (shell-command-to-string cmd)
  444. expired-recordings)
  445. ;; handler - return empty list in case of error
  446. (lambda (key cmd str)
  447. '())))))))
  448. (define* (mythconverg-update-deleted-ltbtranscodedtobedeleted bnl #:optional (continuation mythconverg-execute))
  449. (let ((sql-cmds (map (lambda(f) (simple-format #f "update ltbtranscodedtobedeleted set deleted=1 where basename='~a';" f)) bnl)))
  450. (unless (null? bnl)
  451. (mysql-start-transaction (string-join sql-cmds "\n"))
  452. (mysql-commit "")
  453. (continuation mysql-cmd))))
  454. (define* (myth-clean-recordings-update-deleted-ltbtranscodedtobedeleted #:optional (expiration-date 1) (shell-cmd "rm -f") (continuation mythconverg-execute))
  455. (mythconverg-update-deleted-ltbtranscodedtobedeleted
  456. (myth-clean-recording-directory expiration-date shell-cmd) continuation))
  457. (define (myth-merge-two-recordings chanid starttime)
  458. (let* ((rec1+2 (mythconverg-execute+parse (simple-format #f "select basename,title from recorded where chanid=~a and starttime >= ~a order by starttime limit 2;" chanid starttime)))
  459. (bn1 (caadr rec1+2))
  460. (bn2 (caaddr rec1+2))
  461. (concat (simple-format #f "ffconcat version 1.0\nfile ~a/~a\nfile ~a/~a\n" recordings-directory bn1 recordings-directory bn2))
  462. (port (mkstemp! (mythconverg-input-file-name)))
  463. (tmpfile (port-filename port))
  464. (cmd (simple-format #f "~a -y -safe 0 -f concat -i ~a -codec copy -map 0 ~a/~a && touch --reference='~a/~a' ~a/~a && mv ~a/~a ~a/~a.premerge && mv ~a/~a ~a/." ffmpeg-bin tmpfile working-directory bn1 recordings-directory bn1 working-directory bn1 recordings-directory bn1 recordings-directory bn1 working-directory bn1 recordings-directory)))
  465. (simple-format port concat) (force-output port) (close-port port)
  466. (shell-command-to-string cmd)
  467. (mythutil-rebuild (re-create-record-data (list (cons 'chanid chanid) (cons 'starttime starttime))))
  468. cmd))
  469. (define (ffmpeg-remux-to-h264-ts chanid starttime)
  470. (let* ((r (mythconverg-get-recorded chanid starttime))
  471. (bn (assv-ref r 'basename))
  472. (infile (simple-format #f "~a/~a" recordings-directory bn))
  473. (outfile (simple-format #f "~a/~a" working-directory (recording-new-file-name bn remuxed-ts-file-ending)))
  474. (remux-cmd (simple-format #f "~a -y -i ~a -codec copy -map 0 -f mpegts ~a && touch --reference='~a' ~a && mv ~a ~a/." ffmpeg-bin infile outfile infile outfile outfile recordings-directory))
  475. (bn-cmd (simple-format #f "update recorded set basename='~a' where chanid=~a and starttime='~a';" (recording-new-file-name bn remuxed-ts-file-ending) chanid starttime)))
  476. (shell-command-to-string remux-cmd)
  477. (mythconverg-execute bn-cmd)
  478. (mythutil-rebuild (re-create-record-data r))))
  479. (define (ffmpeg-remux->concat chanid starttime)
  480. (let* ((r (mythconverg-get-recorded chanid starttime))
  481. (bn (assv-ref r 'basename))
  482. (infile (simple-format #f "~a/~a" recordings-directory bn))
  483. (outfile (simple-format #f "~a/~a" working-directory (recording-new-file-name bn transcoded-file-ending)))
  484. (remux-cmd (simple-format #f "~a -y -i ~a -codec copy -map 0 -f mpegts ~a && touch --reference='~a' ~a && mv ~a ~a/." ffmpeg-bin infile outfile infile outfile outfile recordings-directory)))
  485. (shell-command-to-string remux-cmd)
  486. (mythutil-rebuild (re-create-record-data r))))
  487. (define (mythutil-rebuild rec)
  488. (let* ((chanid (recording-chanid rec))
  489. (starttime (recording-starttime rec)))
  490. (define (rebuild tbl)
  491. (shell-command-to-string (simple-format #f "mythutil --chanid=~a --starttime='~a' --clear~a" chanid starttime tbl)))
  492. (map rebuild '(skiplist cutlist seektable))
  493. (shell-command-to-string (simple-format #f "mythcommflag --chanid=~a --starttime='~a' --rebuild" chanid starttime))))
  494. (define* (mythconverg-pending-jobs #:optional (days 7))
  495. (let ((basenames (mythconverg-execute+parse (simple-format #f "select basename from recorded where cutlist=1 and lastmodified>=now() - interval ~a day and not(recgroup = 'Deleted') order by starttime;" days))))
  496. (define (parse-basename bn)
  497. (read-from-string (string-split (car (string-split (car bn) #\.)) #\_)))
  498. (cond ((null? basenames)
  499. (simple-format #t "mythconverg-pending-jobs: no jobs.\n") (force-output) '())
  500. (#t (map parse-basename (cdr basenames))))))
  501. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ROLLBACK ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  502. ;; The recording records for old+new are written to the log file after a ;;
  503. ;; transcoding. Use rollback-transcoding to rollback to the old recording. ;;
  504. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  505. (define* (rollback-transcoding old+new #:optional (update-database #f))
  506. ;; rec = old recording
  507. ;; tr-rec = transcoded recording
  508. (let* ((rec (re-create-record-data (car old+new)))
  509. (tr-rec (re-create-record-data (cadr old+new))))
  510. (catch #t (lambda ()
  511. ;; mysql-start-transaction here
  512. (mysql-start-transaction "")
  513. (mythconverg-update-recorded-seek-table rec)
  514. (mythconverg-update-recordedmarkup-table-rollback rec)
  515. ;; last arg is #f so that no mv/touch is done
  516. (mythconverg-update-recorded-table tr-rec rec #f)
  517. ;; mysql-commit in this step
  518. (if update-database (mythconverg-execute mysql-cmd))
  519. (list 'updated (recording-basename rec)))
  520. (lambda (key . args)
  521. (list 'error (recording-basename rec))))))
  522. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; MAIN ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  523. (define (ffmpeg-myth0 chanid starttime cut-or-skip)
  524. (let* ((rec (create-record-data chanid starttime cut-or-skip))
  525. (cmd (ffmpeg-split+transcode-recording rec))
  526. (tr-rec (create-record-data-for-transcode rec)))
  527. (catch #t (lambda ()
  528. ;; mysql-start-transaction here
  529. (mysql-start-transaction "")
  530. (mythconverg-update-recorded-seek-table tr-rec)
  531. (mythconverg-update-recordedmarkup-table tr-rec)
  532. (mythconverg-update-ltbtranscodedtobedeleted rec)
  533. ;; mysql-commit in this step
  534. (mythconverg-update-recorded-table rec tr-rec)
  535. (mythconverg-execute mysql-cmd)
  536. (list rec tr-rec))
  537. (lambda (key . args)
  538. (list rec tr-rec 'error)))))
  539. (define (ffmpeg-myth-cut c-s)
  540. (ffmpeg-myth0 (car c-s) (cadr c-s) 'cut))
  541. (define (ffmpeg-myth-skip c-s)
  542. (ffmpeg-myth0 (car c-s) (cadr c-s) 'skip))
  543. (define* (ffmpeg-myth-do-pending-jobs #:optional (days 7))
  544. (let ((days (if (number? days) days 7)))
  545. (map ffmpeg-myth-cut (mythconverg-pending-jobs days))))
  546. (define* (tmp-test #:optional (x 7))
  547. (simple-format #t "~a\n" x) (force-output))
  548. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; MAIN ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  549. (define (ffmpeg-myth args)
  550. (display args) (newline)
  551. ;;(shell-command-to-string #t "printenv") (newline)
  552. (let* ((rgs (read-from-string (cdr args)))
  553. (cmd (eval (car rgs) (interaction-environment)))
  554. (result
  555. (cond ((null? rgs) '())
  556. ((null? (cdr rgs)) (cmd))
  557. (#t (apply cmd (cdr rgs))))))
  558. (simple-format #t "~a" result) (force-output)
  559. result))