mirror of
https://gitea.phreedom.club/localhost_frssoft/funkwlmpv
synced 2024-11-22 14:49:22 +02:00
streamsave updated; some caching fixes (may be)
This commit is contained in:
parent
9f37a52a64
commit
add2ef572c
|
@ -7,13 +7,14 @@ from shutil import get_terminal_size
|
|||
from shlex import quote
|
||||
import mpv
|
||||
import time
|
||||
import sys
|
||||
import re
|
||||
|
||||
fzf = FzfPrompt()
|
||||
|
||||
if get_config('enable_persistent_cache'):
|
||||
player = mpv.MPV(cache=True, scripts='src/mpv_scripts/mpv_cache.lua:src/mpv_scripts/streamsave.lua')
|
||||
player = mpv.MPV(cache=True,
|
||||
scripts='src/mpv_scripts/mpv_cache.lua:src/mpv_scripts/streamsave.lua',
|
||||
script_opts='streamsave-save_directory=cache,streamsave-dump_mode=continuous,streansave-force_extension=.mkv,streamsave-autostart=no,output_label=overwrite')
|
||||
player.command('script-message', 'streamsave-path', 'cache')
|
||||
else:
|
||||
player = mpv.MPV(cache=True, demuxer_max_bytes=25*1024*1024)
|
||||
|
|
|
@ -25,18 +25,22 @@ end
|
|||
|
||||
|
||||
function make_cache_track(url)
|
||||
mp.command('script-message streamsave-autostart no')
|
||||
find_uuid = "%x+-%x+-%x+-%x+-%x+"
|
||||
uuid = string.sub(url, string.find(url, find_uuid))
|
||||
host = get_url_host(url)
|
||||
cache_path_file = 'cache/' .. host .. '/' .. uuid .. '.mkv'
|
||||
cache_path_named_file = 'cache/' .. host .. '/' .. uuid .. '.mkv'
|
||||
if false == file_exists(cache_path_file) then
|
||||
createDir('cache/' .. host .. '/')
|
||||
msg.verbose('Caching ' .. cache_path_file .. '')
|
||||
mp.command('script-message streamsave-title ' .. uuid .. '')
|
||||
mp.command('script-message streamsave-force_title ' .. uuid .. '')
|
||||
mp.command('script-message streamsave-label overwrite')
|
||||
mp.set_property('script-opts/media-uuid', uuid)
|
||||
mp.command('script-message streamsave-extension .mkv')
|
||||
mp.command('script-message streamsave-path cache/' .. host .. '')
|
||||
mp.command('script-message streamsave-rec')
|
||||
mp.command('script-message streamsave-autostart yes')
|
||||
else
|
||||
msg.verbose('Already cached ' .. cache_path_file .. '')
|
||||
os.execute('touch ' .. cache_path_file .. '')
|
||||
|
@ -52,3 +56,11 @@ mp.add_hook("on_load", 11, function()
|
|||
make_cache_track(url)
|
||||
end
|
||||
end)
|
||||
|
||||
mp.register_event("file-loaded", function()
|
||||
msg.verbose('reusable cache post-hook activated')
|
||||
local url = mp.get_property("stream-open-filename", "")
|
||||
if true == (url:find("https?://") == 1) then
|
||||
make_cache_track(url)
|
||||
end
|
||||
end)
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
--[[
|
||||
|
||||
streamsave.lua
|
||||
Version 0.20.6
|
||||
2022-10-13
|
||||
Version 0.23.2
|
||||
2023-5-21
|
||||
https://github.com/Sagnac/streamsave
|
||||
NOTE: Modified
|
||||
|
||||
mpv script aimed at saving live streams and clipping online videos without encoding.
|
||||
|
||||
|
@ -12,10 +11,12 @@ Essentially a wrapper around mpv's cache dumping commands, the script adds the f
|
|||
|
||||
* Automatic determination of the output file name and format
|
||||
* Option to specify the preferred output directory
|
||||
* Switch between 3 different dump modes (clip mode, full/continuous dump, write from beginning to current position)
|
||||
* Switch between 5 different dump modes:
|
||||
(clip mode, full/continuous dump, write from beginning to current position, current chapter, all chapters)
|
||||
* Prevention of file overwrites
|
||||
* Acceptance of inverted loop ranges, allowing the end point to be set first
|
||||
* Dynamic chapter indicators on the OSC displaying the clipping interval
|
||||
* Option to track HLS packet drops
|
||||
* Automated stream saving
|
||||
* Workaround for some DAI HLS streams served from .m3u8 where the host changes
|
||||
|
||||
|
@ -51,15 +52,27 @@ and only set the first loop point then press the cache-write keybind.
|
|||
|
||||
dump_mode=current will dump the cache from timestamp 0 to the current playback position in the file.
|
||||
|
||||
dump_mode=chapter will write the current chapter to file.
|
||||
|
||||
dump_mode=segments writes out all chapters to individual files.
|
||||
|
||||
If you wish to output a single chapter using a numerical input instead you can specify it with a command at runtime:
|
||||
script-message streamsave-chapter 7
|
||||
|
||||
The output_label option allows you to choose how the output filename is tagged.
|
||||
The default uses iterated step increments for every file output; i.e. file-1.mkv, file-2.mkv, etc.
|
||||
|
||||
There are 3 other choices:
|
||||
There are 4 other choices:
|
||||
|
||||
output_label=timestamp will append Unix timestamps to the file name.
|
||||
|
||||
output_label=range will tag the file with the A-B loop range instead using the format HH.MM.SS
|
||||
e.g. file-[00.15.00 - 00.20.00].mkv
|
||||
|
||||
output_label=overwrite will not tag the file and will overwrite any existing files with the same name.
|
||||
|
||||
output_label=chapter uses the chapter title for the file name if using one of the chapter modes.
|
||||
|
||||
The force_extension option allows you to force a preferred format and sidestep the automatic detection.
|
||||
If using this option it is recommended that a highly flexible container is used (e.g. Matroska).
|
||||
The format is specified as the extension including the dot (e.g. force_extension=.mkv).
|
||||
|
@ -78,6 +91,8 @@ Once the A-B points are cleared the original chapters are restored.
|
|||
Any chapters added after A-B mode is entered are added to the initial chapter list.
|
||||
This option is disabled by default; set range_marks=yes in streamsave.conf in order to enable it.
|
||||
|
||||
The track_packets option adds chapters to positions where packet loss occurs for HLS streams.
|
||||
|
||||
Automation Options:
|
||||
|
||||
The autostart and autoend options are used for automated stream capturing.
|
||||
|
@ -88,6 +103,8 @@ to stop at that time.
|
|||
The hostchange option enables an experimental workaround for DAI HLS .m3u8 streams in which the host changes.
|
||||
If enabled this will result in multiple files being output as the stream reloads.
|
||||
The autostart option must also be enabled in order to autosave these types of streams.
|
||||
The `on_demand` option is a suboption of the hostchange option which, if enabled, triggers reloads immediately across
|
||||
segment switches without waiting until playback has reached the end of the last segment.
|
||||
|
||||
The `quit=HH:MM:SS` option will set a one shot timer from script load to the specified time,
|
||||
at which point the player will exit. This serves as a replacement for autoend when using hostchange.
|
||||
|
@ -122,14 +139,16 @@ local unpack = unpack or table.unpack
|
|||
-- change these in streamsave.conf
|
||||
local opts = {
|
||||
save_directory = [[.]], -- output file directory
|
||||
dump_mode = "continuous", -- <ab|current|continuous>
|
||||
output_label = "overwrite", -- <increment|range|timestamp|overwrite>
|
||||
force_extension = ".mkv", -- <no|.ext> extension will be .ext if set
|
||||
dump_mode = "ab", -- <ab|current|continuous|chapter|segments>
|
||||
output_label = "increment", -- <increment|range|timestamp|overwrite|chapter>
|
||||
force_extension = "no", -- <no|.ext> extension will be .ext if set
|
||||
force_title = "no", -- <no|title> custom title used for the filename
|
||||
range_marks = false, -- <yes|no> set chapters at A-B loop points?
|
||||
autostart = true, -- <yes|no> automatically dump cache at start?
|
||||
track_packets = false, -- <yes|no> track HLS packet drops
|
||||
autostart = false, -- <yes|no> automatically dump cache at start?
|
||||
autoend = "no", -- <no|HH:MM:SS> cache time to stop at
|
||||
hostchange = false, -- <yes|no> use if the host changes mid stream
|
||||
on_demand = false, -- <yes|no> hostchange suboption, instant reloads
|
||||
quit = "no", -- <no|HH:MM:SS> quits player at specified time
|
||||
piecewise = false, -- <yes|no> writes stream in parts with autoend
|
||||
}
|
||||
|
@ -141,8 +160,12 @@ local file = {
|
|||
title, -- media title
|
||||
inc, -- filename increments
|
||||
ext, -- file extension
|
||||
loaded, -- flagged once the initial load has taken place
|
||||
pending, -- number of files pending write completion (max 2)
|
||||
queue, -- cache_write queue in case of multiple write requests
|
||||
writing, -- file writing object returned by the write command
|
||||
quitsec, -- user specified quit time in seconds
|
||||
quit_timer, -- player quit timer set according to quitsec
|
||||
oldtitle, -- initialized if title is overridden, allows revert
|
||||
oldext, -- initialized if format is overridden, allows revert
|
||||
oldpath, -- initialized if directory is overriden, allows revert
|
||||
|
@ -155,42 +178,49 @@ local loop = {
|
|||
b_revert, -- B loop point prior to keyframe alignment
|
||||
range, -- A-B loop range
|
||||
aligned, -- are the loop points aligned to keyframes?
|
||||
continuous, -- is the writing continuous?
|
||||
}
|
||||
|
||||
local cache = {
|
||||
dumped, -- autowrite cache state (serves as an autowrite request)
|
||||
observed, -- whether the cache time is being observed
|
||||
endsec, -- user specified autoend cache time in seconds
|
||||
prior, -- previous cache time
|
||||
prior, -- cache duration prior to staging the seamless reload mechanism
|
||||
seekend, -- seekable cache end timestamp
|
||||
part, -- approx. end time of last piece / start time of next piece
|
||||
switch, -- request to observe track switches and seeking
|
||||
use, -- use cache_time instead of seekend for initial piece
|
||||
restart, -- hostchange interval where subsequent reloads are immediate
|
||||
id, -- number of times the packet tracking event has fired
|
||||
packets, -- table of periodic timers indexed by cache id stamps
|
||||
}
|
||||
|
||||
local convert_time
|
||||
local observe_cache
|
||||
local continuous
|
||||
local write_file
|
||||
local reset
|
||||
local title_change
|
||||
local container
|
||||
local track = {
|
||||
vid, -- video track id
|
||||
aid, -- audio track id
|
||||
sid, -- subtitle track id
|
||||
restart, -- hostchange interval where subsequent reloads are immediate
|
||||
suspend, -- suspension interval on track-list changes
|
||||
}
|
||||
|
||||
local segments = {} -- chapter segments set for writing
|
||||
local chapter_list = {} -- initial chapter list
|
||||
local ab_chapters = {} -- A-B loop point chapters
|
||||
local chapter_points
|
||||
local get_seekable_cache
|
||||
local reload
|
||||
local automatic
|
||||
local quitseconds
|
||||
local quit_timer
|
||||
local autoquit
|
||||
|
||||
function convert_time(value)
|
||||
local i, j, H, M, S = value:find("(%d+):(%d+):(%d+)")
|
||||
if not i then
|
||||
return
|
||||
else
|
||||
local title_change
|
||||
local container
|
||||
local get_chapters
|
||||
local chapter_points
|
||||
local reset
|
||||
local get_seekable_cache
|
||||
local automatic
|
||||
local autoquit
|
||||
local packet_events
|
||||
local observe_cache
|
||||
local observe_tracks
|
||||
|
||||
local function convert_time(value)
|
||||
local H, M, S = value:match("^(%d+):([0-5]%d):([0-5]%d)$")
|
||||
if H then
|
||||
return H*3600 + M*60 + S
|
||||
end
|
||||
end
|
||||
|
@ -199,16 +229,19 @@ local function validate_opts()
|
|||
if opts.output_label ~= "increment" and
|
||||
opts.output_label ~= "range" and
|
||||
opts.output_label ~= "timestamp" and
|
||||
opts.output_label ~= "overwrite"
|
||||
opts.output_label ~= "overwrite" and
|
||||
opts.output_label ~= "chapter"
|
||||
then
|
||||
msg.warn("Invalid output_label '" .. opts.output_label .. "'")
|
||||
msg.error("Invalid output_label '" .. opts.output_label .. "'")
|
||||
opts.output_label = "increment"
|
||||
end
|
||||
if opts.dump_mode ~= "ab" and
|
||||
opts.dump_mode ~= "current" and
|
||||
opts.dump_mode ~= "continuous"
|
||||
opts.dump_mode ~= "continuous" and
|
||||
opts.dump_mode ~= "chapter" and
|
||||
opts.dump_mode ~= "segments"
|
||||
then
|
||||
msg.warn("Invalid dump_mode '" .. opts.dump_mode .. "'")
|
||||
msg.error("Invalid dump_mode '" .. opts.dump_mode .. "'")
|
||||
opts.dump_mode = "ab"
|
||||
end
|
||||
if opts.autoend ~= "no" then
|
||||
|
@ -216,15 +249,15 @@ local function validate_opts()
|
|||
cache.endsec = convert_time(opts.autoend)
|
||||
end
|
||||
if not convert_time(opts.autoend) then
|
||||
msg.warn("Invalid autoend value '" .. opts.autoend ..
|
||||
msg.error("Invalid autoend value '" .. opts.autoend ..
|
||||
"'. Use HH:MM:SS format.")
|
||||
opts.autoend = "no"
|
||||
end
|
||||
end
|
||||
if opts.quit ~= "no" then
|
||||
quitseconds = convert_time(opts.quit)
|
||||
if not quitseconds then
|
||||
msg.warn("Invalid quit value '" .. opts.quit ..
|
||||
file.quitsec = convert_time(opts.quit)
|
||||
if not file.quitsec then
|
||||
msg.error("Invalid quit value '" .. opts.quit ..
|
||||
"'. Use HH:MM:SS format.")
|
||||
opts.quit = "no"
|
||||
end
|
||||
|
@ -249,17 +282,22 @@ local function update_opts(changed)
|
|||
if opts.range_marks then
|
||||
chapter_points()
|
||||
else
|
||||
ab_chapters = {}
|
||||
if not get_chapters() then
|
||||
mp.set_property_native("chapter-list", chapter_list)
|
||||
end
|
||||
ab_chapters = {}
|
||||
end
|
||||
end
|
||||
if changed["autoend"] then
|
||||
cache.endsec = convert_time(opts.autoend)
|
||||
observe_cache()
|
||||
end
|
||||
if changed["autostart"] or changed["hostchange"] then
|
||||
if changed["autostart"] then
|
||||
observe_cache()
|
||||
end
|
||||
if changed["hostchange"] then
|
||||
observe_tracks(opts.hostchange)
|
||||
end
|
||||
if changed["quit"] then
|
||||
autoquit()
|
||||
end
|
||||
|
@ -268,6 +306,9 @@ local function update_opts(changed)
|
|||
elseif changed["piecewise"] then
|
||||
cache.endsec = convert_time(opts.autoend)
|
||||
end
|
||||
if changed["track_packets"] then
|
||||
packet_events(opts.track_packets)
|
||||
end
|
||||
end
|
||||
|
||||
options.read_options(opts, "streamsave", update_opts)
|
||||
|
@ -281,6 +322,10 @@ local function mode_switch(value)
|
|||
value = "current"
|
||||
elseif opts.dump_mode == "current" then
|
||||
value = "continuous"
|
||||
elseif opts.dump_mode == "continuous" then
|
||||
value = "chapter"
|
||||
elseif opts.dump_mode == "chapter" then
|
||||
value = "segments"
|
||||
else
|
||||
value = "ab"
|
||||
end
|
||||
|
@ -297,8 +342,16 @@ local function mode_switch(value)
|
|||
opts.dump_mode = "current"
|
||||
print("Current position mode")
|
||||
mp.osd_message("Cache write mode: Current position")
|
||||
elseif value == "chapter" then
|
||||
opts.dump_mode = "chapter"
|
||||
print("Chapter mode (single chapter)")
|
||||
mp.osd_message("Cache write mode: Chapter")
|
||||
elseif value == "segments" then
|
||||
opts.dump_mode = "segments"
|
||||
print("Segments mode (all chapters)")
|
||||
mp.osd_message("Cache write mode: Segments")
|
||||
else
|
||||
msg.warn("Invalid dump mode '" .. value .. "'")
|
||||
msg.error("Invalid dump mode '" .. value .. "'")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -321,12 +374,13 @@ function container(_, _, req)
|
|||
local file_format = mp.get_property("file-format")
|
||||
if not file_format then
|
||||
reset()
|
||||
observe_tracks()
|
||||
return end
|
||||
if opts.force_extension ~= "no" and not req then
|
||||
file.ext = opts.force_extension
|
||||
observe_cache()
|
||||
return end
|
||||
if string.find(file_format, "mp4")
|
||||
if string.match(file_format, "mp4")
|
||||
or ((video == "h264" or video == "av1" or not video) and
|
||||
(audio == "aac" or not audio))
|
||||
then
|
||||
|
@ -339,6 +393,7 @@ function container(_, _, req)
|
|||
file.ext = ".mkv"
|
||||
end
|
||||
observe_cache()
|
||||
observe_tracks()
|
||||
file.oldext = nil
|
||||
end
|
||||
|
||||
|
@ -395,6 +450,8 @@ local function label_override(value)
|
|||
value = "timestamp"
|
||||
elseif opts.output_label == "timestamp" then
|
||||
value = "overwrite"
|
||||
elseif opts.output_label == "overwrite" then
|
||||
value = "chapter"
|
||||
else
|
||||
value = "increment"
|
||||
end
|
||||
|
@ -408,8 +465,10 @@ end
|
|||
local function marks_override(value)
|
||||
if not value or value == "no" then
|
||||
opts.range_marks = false
|
||||
ab_chapters = {}
|
||||
if not get_chapters() then
|
||||
mp.set_property_native("chapter-list", chapter_list)
|
||||
end
|
||||
ab_chapters = {}
|
||||
print("Range marks disabled")
|
||||
mp.osd_message("streamsave: range marks disabled")
|
||||
elseif value == "yes" then
|
||||
|
@ -418,17 +477,12 @@ local function marks_override(value)
|
|||
print("Range marks enabled")
|
||||
mp.osd_message("streamsave: range marks enabled")
|
||||
else
|
||||
msg.warn("Invalid input '" .. value .. "'. Use yes or no.")
|
||||
msg.error("Invalid input '" .. value .. "'. Use yes or no.")
|
||||
mp.osd_message("streamsave: invalid input; use yes or no")
|
||||
end
|
||||
end
|
||||
|
||||
local function autostart_override(value)
|
||||
if value and value ~= "no" and value ~= "yes" then
|
||||
msg.warn("Invalid input '" .. value .. "'. Use yes or no.")
|
||||
mp.osd_message("streamsave: invalid input; use yes or no")
|
||||
return
|
||||
end
|
||||
if not value or value == "no" then
|
||||
opts.autostart = false
|
||||
print("Autostart disabled")
|
||||
|
@ -437,6 +491,10 @@ local function autostart_override(value)
|
|||
opts.autostart = true
|
||||
print("Autostart enabled")
|
||||
mp.osd_message("streamsave: autostart enabled")
|
||||
else
|
||||
msg.error("Invalid input '" .. value .. "'. Use yes or no.")
|
||||
mp.osd_message("streamsave: invalid input; use yes or no")
|
||||
return
|
||||
end
|
||||
observe_cache()
|
||||
end
|
||||
|
@ -451,24 +509,31 @@ local function autoend_override(value)
|
|||
end
|
||||
|
||||
local function hostchange_override(value)
|
||||
local hostchange = opts.hostchange
|
||||
value = value == "cycle" and (not opts.hostchange and "yes" or "no") or value
|
||||
if value and value ~= "no" and value ~= "yes" then
|
||||
msg.warn("Invalid input '" .. value .. "'. Use yes or no.")
|
||||
mp.osd_message("streamsave: invalid input; use yes or no")
|
||||
return
|
||||
end
|
||||
if not value or value == "no" then
|
||||
opts.hostchange = false
|
||||
mp.unobserve_property(reload)
|
||||
local timer = cache.restart and cache.restart:kill()
|
||||
print("Hostchange disabled")
|
||||
mp.osd_message("streamsave: hostchange disabled")
|
||||
elseif value == "yes" then
|
||||
opts.hostchange = true
|
||||
print("Hostchange enabled")
|
||||
mp.osd_message("streamsave: hostchange enabled")
|
||||
elseif value == "on_demand" then
|
||||
opts.on_demand = not opts.on_demand
|
||||
opts.hostchange = opts.on_demand or opts.hostchange
|
||||
local status = opts.on_demand and "enabled" or "disabled"
|
||||
print("Hostchange: On Demand " .. status)
|
||||
mp.osd_message("streamsave: hostchange on_demand " .. status)
|
||||
else
|
||||
local allowed = "yes, no, cycle, or on_demand"
|
||||
msg.error("Invalid input '" .. value .. "'. Use " .. allowed .. ".")
|
||||
mp.osd_message("streamsave: invalid input; use " .. allowed)
|
||||
return
|
||||
end
|
||||
if opts.hostchange ~= hostchange then
|
||||
observe_tracks(opts.hostchange)
|
||||
end
|
||||
observe_cache()
|
||||
end
|
||||
|
||||
local function quit_override(value)
|
||||
|
@ -491,11 +556,33 @@ local function piecewise_override(value)
|
|||
print("Piecewise dumping enabled")
|
||||
mp.osd_message("streamsave: piecewise dumping enabled")
|
||||
else
|
||||
msg.warn("Invalid input '" .. value .. "'. Use yes or no.")
|
||||
msg.error("Invalid input '" .. value .. "'. Use yes or no.")
|
||||
mp.osd_message("streamsave: invalid input; use yes or no")
|
||||
end
|
||||
end
|
||||
|
||||
local function packet_override(value)
|
||||
local track_packets = opts.track_packets
|
||||
if value == "cycle" then
|
||||
value = not track_packets and "yes" or "no"
|
||||
end
|
||||
if not value or value == "no" then
|
||||
opts.track_packets = false
|
||||
print("Track packets disabled")
|
||||
mp.osd_message("streamsave: track packets disabled")
|
||||
elseif value == "yes" then
|
||||
opts.track_packets = true
|
||||
print("Track packets enabled")
|
||||
mp.osd_message("streamsave: track packets enabled")
|
||||
else
|
||||
msg.error("Invalid input '" .. value .. "'. Use yes or no.")
|
||||
mp.osd_message("streamsave: invalid input; use yes or no")
|
||||
end
|
||||
if opts.track_packets ~= track_packets then
|
||||
packet_events(opts.track_packets)
|
||||
end
|
||||
end
|
||||
|
||||
local function range_flip()
|
||||
loop.a = mp.get_property_number("ab-loop-a")
|
||||
loop.b = mp.get_property_number("ab-loop-b")
|
||||
|
@ -550,6 +637,47 @@ local function range_stamp(mode)
|
|||
end
|
||||
end
|
||||
|
||||
local function write_chapter(chapter)
|
||||
get_chapters()
|
||||
if chapter_list[chapter] or chapter == 0 then
|
||||
segments[1] = {
|
||||
["start"] = chapter == 0 and 0 or chapter_list[chapter]["time"],
|
||||
["end"] = chapter_list[chapter + 1]
|
||||
and chapter_list[chapter + 1]["time"]
|
||||
or mp.get_property_number("duration", "no"),
|
||||
["title"] = chapter .. ". " .. (chapter ~= 0
|
||||
and chapter_list[chapter]["title"] or file.title)
|
||||
}
|
||||
print("Writing chapter " .. chapter .. " ....")
|
||||
return true
|
||||
else
|
||||
msg.error("Chapter not found.")
|
||||
end
|
||||
end
|
||||
|
||||
local function extract_segments(n)
|
||||
for i = 1, n - 1 do
|
||||
segments[i] = {
|
||||
["start"] = chapter_list[i]["time"],
|
||||
["end"] = chapter_list[i + 1]["time"],
|
||||
["title"] = i .. ". " .. (chapter_list[i]["title"] or file.title)
|
||||
}
|
||||
end
|
||||
if chapter_list[1]["time"] ~= 0 then
|
||||
table.insert(segments, 1, {
|
||||
["start"] = 0,
|
||||
["end"] = chapter_list[1]["time"],
|
||||
["title"] = "0. " .. file.title
|
||||
})
|
||||
end
|
||||
table.insert(segments, {
|
||||
["start"] = chapter_list[n]["time"],
|
||||
["end"] = mp.get_property_number("duration", "no"),
|
||||
["title"] = n .. ". " .. (chapter_list[n]["title"] or file.title)
|
||||
})
|
||||
print("Writing out all " .. #segments .. " chapters to separate files ....")
|
||||
end
|
||||
|
||||
local function write_set(mode, file_name, file_pos, quiet)
|
||||
local command = {
|
||||
_flags = {
|
||||
|
@ -559,6 +687,11 @@ local function write_set(mode, file_name, file_pos, quiet)
|
|||
}
|
||||
if mode == "ab" then
|
||||
command["name"] = "ab-loop-dump-cache"
|
||||
elseif (mode == "chapter" or mode == "segments") and segments[1] then
|
||||
command["name"] = "dump-cache"
|
||||
command["start"] = segments[1]["start"]
|
||||
command["end"] = segments[1]["end"]
|
||||
table.remove(segments, 1)
|
||||
else
|
||||
command["name"] = "dump-cache"
|
||||
command["start"] = 0
|
||||
|
@ -567,41 +700,8 @@ local function write_set(mode, file_name, file_pos, quiet)
|
|||
return command
|
||||
end
|
||||
|
||||
local function cache_write(mode, quiet)
|
||||
if not (file.title and file.ext) then
|
||||
return end
|
||||
if file.pending == 2 then
|
||||
file.queue = file.queue or {}
|
||||
-- honor extra write requests when pending queue is full
|
||||
-- but limit number of outstanding write requests to be fulfilled
|
||||
if #file.queue < 10 then
|
||||
table.insert(file.queue, {mode, quiet})
|
||||
end
|
||||
return end
|
||||
range_flip()
|
||||
-- evaluate tagging conditions and set file name
|
||||
if opts.output_label == "increment" then
|
||||
increment_filename()
|
||||
elseif opts.output_label == "range" then
|
||||
range_stamp(mode)
|
||||
elseif opts.output_label == "timestamp" then
|
||||
file.name = set_name(-os.time())
|
||||
elseif opts.output_label == "overwrite" then
|
||||
file.name = set_name("")
|
||||
end
|
||||
-- dump cache according to mode
|
||||
local file_pos
|
||||
local file_name = file.name -- scope reduction so callback verifies correct file
|
||||
file.pending = (file.pending or 0) + 1
|
||||
continuous = mode == "continuous" or loop.a and not loop.b
|
||||
if mode == "current" then
|
||||
file_pos = mp.get_property_number("playback-time", 0)
|
||||
elseif continuous and file.pending == 1 then
|
||||
print("Dumping cache continuously to:" .. file_name)
|
||||
end
|
||||
write_file = mp.command_native_async (
|
||||
write_set(mode, file_name, file_pos, quiet),
|
||||
function(success, _, command_error)
|
||||
local function on_write_finish(cache_write, mode, file_name)
|
||||
return function(success, _, command_error)
|
||||
command_error = command_error and msg.error(command_error)
|
||||
-- check if file is written
|
||||
if utils.file_info(file_name) then
|
||||
|
@ -613,17 +713,84 @@ local function cache_write(mode, quiet)
|
|||
else
|
||||
msg.error("File not written.")
|
||||
end
|
||||
if continuous and file.pending == 2 then
|
||||
if loop.continuous and file.pending == 2 then
|
||||
print("Dumping cache continuously to: " .. file.name)
|
||||
end
|
||||
file.pending = file.pending - 1
|
||||
-- fulfil any write requests now that the pending queue has been serviced
|
||||
if file.queue and #file.queue > 0 then
|
||||
cache_write(file.queue[1][1], file.queue[1][2])
|
||||
if next(segments) then
|
||||
cache_write("segments", true)
|
||||
elseif mode == "segments" then
|
||||
mp.osd_message("Cache dumping successfully ended.")
|
||||
end
|
||||
if file.queue and next(file.queue) and not segments[1] then
|
||||
cache_write(unpack(file.queue[1]))
|
||||
table.remove(file.queue, 1)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
local function cache_write(mode, quiet, chapter)
|
||||
if not (file.title and file.ext) then
|
||||
return end
|
||||
if file.pending == 2
|
||||
or segments[1] and file.pending > 0 and not loop.continuous
|
||||
then
|
||||
file.queue = file.queue or {}
|
||||
-- honor extra write requests when pending queue is full
|
||||
-- but limit number of outstanding write requests to be fulfilled
|
||||
if #file.queue < 10 then
|
||||
table.insert(file.queue, {mode, quiet, chapter})
|
||||
end
|
||||
return end
|
||||
range_flip()
|
||||
-- set the output list for the chapter modes
|
||||
if mode == "segments" and not segments[1] then
|
||||
get_chapters()
|
||||
local n = #chapter_list
|
||||
if n > 0 then
|
||||
extract_segments(n)
|
||||
quiet = true
|
||||
mp.osd_message("Cache dumping started.")
|
||||
else
|
||||
mode = "continuous"
|
||||
end
|
||||
elseif mode == "chapter" and not segments[1] then
|
||||
chapter = chapter or mp.get_property_number("chapter", -1) + 1
|
||||
if not write_chapter(chapter) then
|
||||
return
|
||||
end
|
||||
end
|
||||
-- evaluate tagging conditions and set file name
|
||||
if opts.output_label == "increment" then
|
||||
increment_filename()
|
||||
elseif opts.output_label == "range" then
|
||||
range_stamp(mode)
|
||||
elseif opts.output_label == "timestamp" then
|
||||
file.name = set_name(-os.time())
|
||||
elseif opts.output_label == "overwrite" then
|
||||
file.name = set_name("")
|
||||
elseif opts.output_label == "chapter" then
|
||||
if segments[1] then
|
||||
file.name = file.path .. "/" .. segments[1]["title"] .. file.ext
|
||||
else
|
||||
increment_filename()
|
||||
end
|
||||
end
|
||||
-- dump cache according to mode
|
||||
local file_pos
|
||||
file.pending = (file.pending or 0) + 1
|
||||
loop.continuous = mode == "continuous"
|
||||
or mode == "ab" and loop.a and not loop.b
|
||||
or segments[1] and segments[1]["end"] == "no"
|
||||
if mode == "current" then
|
||||
file_pos = mp.get_property_number("playback-time", 0)
|
||||
elseif loop.continuous and file.pending == 1 then
|
||||
print("Dumping cache continuously to: " .. file.name)
|
||||
end
|
||||
local commands = write_set(mode, file.name, file_pos, quiet)
|
||||
local callback = on_write_finish(cache_write, mode, file.name)
|
||||
file.writing = mp.command_native_async(commands, callback)
|
||||
return true
|
||||
end
|
||||
|
||||
|
@ -649,16 +816,15 @@ local function align_cache()
|
|||
end
|
||||
end
|
||||
|
||||
-- creates chapters at A-B loop points
|
||||
function chapter_points()
|
||||
if not opts.range_marks then
|
||||
return end
|
||||
function get_chapters()
|
||||
local current_chapters = mp.get_property_native("chapter-list", {})
|
||||
local updated -- do the stored chapters reflect the current chapters ?
|
||||
-- make sure master list is up to date
|
||||
if current_chapters[1] and
|
||||
if not current_chapters[1] or
|
||||
not string.match(current_chapters[1]["title"], "^[AB] loop point$")
|
||||
then
|
||||
chapter_list = current_chapters
|
||||
updated = true
|
||||
-- if a script has added chapters after A-B points are set then
|
||||
-- add those to the original chapter list
|
||||
elseif #current_chapters > #ab_chapters then
|
||||
|
@ -666,12 +832,22 @@ function chapter_points()
|
|||
table.insert(chapter_list, current_chapters[i])
|
||||
end
|
||||
end
|
||||
return updated
|
||||
end
|
||||
|
||||
-- creates chapters at A-B loop points
|
||||
function chapter_points()
|
||||
if not opts.range_marks then
|
||||
return end
|
||||
local updated = get_chapters()
|
||||
ab_chapters = {}
|
||||
-- restore original chapter list if A-B points are cleared
|
||||
-- otherwise set chapters to A-B points
|
||||
range_flip()
|
||||
if not loop.a and not loop.b then
|
||||
if not updated then
|
||||
mp.set_property_native("chapter-list", chapter_list)
|
||||
end
|
||||
else
|
||||
if loop.a then
|
||||
ab_chapters[1] = {
|
||||
|
@ -696,36 +872,53 @@ end
|
|||
|
||||
-- stops writing the file
|
||||
local function stop()
|
||||
mp.abort_async_command(write_file or {})
|
||||
mp.abort_async_command(file.writing or {})
|
||||
end
|
||||
|
||||
function reset()
|
||||
if cache.observed or cache.dumped then
|
||||
stop()
|
||||
mp.unobserve_property(automatic)
|
||||
mp.unobserve_property(reload)
|
||||
mp.unobserve_property(get_seekable_cache)
|
||||
cache.endsec = convert_time(opts.autoend)
|
||||
cache.observed = false
|
||||
end
|
||||
cache.prior = 0
|
||||
cache.part = 0
|
||||
cache.dumped = false
|
||||
cache.switch = true
|
||||
end
|
||||
reset()
|
||||
|
||||
function get_seekable_cache(prop, range_check, underrun)
|
||||
-- reload on demand (hostchange)
|
||||
local function reload()
|
||||
reset()
|
||||
observe_tracks()
|
||||
msg.warn("Reloading stream due to host change.")
|
||||
mp.command("playlist-play-index current")
|
||||
end
|
||||
|
||||
local function stabilize()
|
||||
if mp.get_property_number("demuxer-cache-time", 0) > 1500 then
|
||||
reload()
|
||||
end
|
||||
end
|
||||
|
||||
local function suspend()
|
||||
if not track.suspend then
|
||||
track.suspend = mp.add_timeout(25, stabilize)
|
||||
else
|
||||
track.suspend:resume()
|
||||
end
|
||||
end
|
||||
|
||||
function get_seekable_cache(prop, range_check)
|
||||
-- use the seekable part of the cache for more accurate timestamps
|
||||
local cache_state = mp.get_property_native("demuxer-cache-state", {})
|
||||
if underrun then
|
||||
return cache_state["underrun"]
|
||||
end
|
||||
local seekable_ranges = cache_state["seekable-ranges"] or {}
|
||||
if prop then
|
||||
if range_check ~= false and
|
||||
(#seekable_ranges == 0
|
||||
or not mp.get_property_number("demuxer-cache-time"))
|
||||
or not cache_state["cache-end"])
|
||||
then
|
||||
reset()
|
||||
cache.use = opts.piecewise
|
||||
|
@ -737,45 +930,67 @@ function get_seekable_cache(prop, range_check, underrun)
|
|||
for i, range in ipairs(seekable_ranges) do
|
||||
seekable_ends[i] = range["end"] or 0
|
||||
end
|
||||
cache.seekend = math.max(0, unpack(seekable_ends))
|
||||
return cache.seekend
|
||||
return math.max(0, unpack(seekable_ends))
|
||||
end
|
||||
|
||||
function reload(_, play_time)
|
||||
local cache_duration = mp.get_property_number("demuxer-cache-duration")
|
||||
if play_time and play_time >= cache.seekend - 0.25
|
||||
or cache_duration and math.abs(cache.prior - cache_duration) > 4800
|
||||
or get_seekable_cache(nil, false, true)
|
||||
-- seamlessly reload on inserts (hostchange)
|
||||
local function seamless(_, cache_state)
|
||||
cache_state = cache_state or {}
|
||||
local reader = math.abs(cache_state["reader-pts"] or 0)
|
||||
local cache_duration = math.abs(cache_state["cache-duration"] or cache.prior)
|
||||
-- wait until playback of the loaded cache has practically ended
|
||||
-- or there's a timestamp reset / position shift
|
||||
if reader >= cache.seekend - 0.25
|
||||
or cache.prior - cache_duration > 3000
|
||||
or cache_state["underrun"]
|
||||
then
|
||||
reload()
|
||||
track.restart = track.restart or mp.add_timeout(300, function() end)
|
||||
track.restart:resume()
|
||||
end
|
||||
end
|
||||
|
||||
-- detect stream switches (hostchange)
|
||||
local function detect()
|
||||
local eq = true
|
||||
local t = {
|
||||
vid = mp.get_property_number("current-tracks/video/id", 0),
|
||||
aid = mp.get_property_number("current-tracks/audio/id", 0),
|
||||
sid = mp.get_property_number("current-tracks/sub/id", 0)
|
||||
}
|
||||
for k, v in pairs(t) do
|
||||
eq = track[k] == v and eq
|
||||
track[k] = v
|
||||
end
|
||||
-- do not initiate a reload process if the track ids do not match
|
||||
-- or the track loading suspension interval is active
|
||||
if not eq then
|
||||
return
|
||||
end
|
||||
if track.suspend:is_enabled() then
|
||||
stabilize()
|
||||
return
|
||||
end
|
||||
-- bifurcate
|
||||
if track.restart and track.restart:is_enabled() then
|
||||
track.restart:kill()
|
||||
reload()
|
||||
elseif opts.on_demand then
|
||||
reload()
|
||||
else
|
||||
-- watch the cache state outside of the interval
|
||||
-- and use it to decide when to reload
|
||||
reset()
|
||||
cache.restart = cache.restart or mp.add_timeout(300, function() end)
|
||||
cache.restart:resume()
|
||||
msg.warn("Reloading stream due to host change.")
|
||||
mp.command("playlist-play-index current")
|
||||
observe_tracks(false)
|
||||
cache.observed = true
|
||||
cache.prior = math.abs(mp.get_property_number("demuxer-cache-duration", 4E3))
|
||||
cache.seekend = get_seekable_cache()
|
||||
mp.observe_property("demuxer-cache-state", "native", seamless)
|
||||
end
|
||||
end
|
||||
|
||||
function automatic(_, cache_time)
|
||||
if opts.hostchange and cache.prior ~= 0
|
||||
and (not cache_time or math.abs(cache_time - cache.prior) > 300
|
||||
or mp.get_property_number("demuxer-cache-duration", 0) > 11000)
|
||||
and not mp.get_property_bool("seeking")
|
||||
then
|
||||
if not cache.restart or not cache.restart:is_enabled() then
|
||||
reset()
|
||||
cache.observed = true
|
||||
cache.prior = mp.get_property_number("demuxer-cache-duration", 0)
|
||||
get_seekable_cache()
|
||||
mp.observe_property("playback-time", "number", reload)
|
||||
else
|
||||
-- reload stream
|
||||
cache.restart:kill()
|
||||
reset()
|
||||
msg.warn("Reloading stream due to host change.")
|
||||
mp.command("playlist-play-index current")
|
||||
end
|
||||
return
|
||||
elseif not cache_time then
|
||||
if not cache_time then
|
||||
reset()
|
||||
cache.use = opts.piecewise
|
||||
observe_cache()
|
||||
|
@ -805,12 +1020,9 @@ function automatic(_, cache_time)
|
|||
mp.observe_property("seeking", "bool", get_seekable_cache)
|
||||
end
|
||||
-- unobserve cache time if not needed
|
||||
if cache.dumped and not cache.switch
|
||||
and not cache.endsec and not opts.hostchange
|
||||
then
|
||||
if cache.dumped and not cache.switch and not cache.endsec then
|
||||
mp.unobserve_property(automatic)
|
||||
cache.observed = false
|
||||
cache.prior = 0
|
||||
return
|
||||
end
|
||||
-- stop cache dump
|
||||
|
@ -830,34 +1042,104 @@ function automatic(_, cache_time)
|
|||
end
|
||||
stop()
|
||||
end
|
||||
cache.prior = cache_time
|
||||
end
|
||||
|
||||
function autoquit()
|
||||
if opts.quit == "no" then
|
||||
if quit_timer then
|
||||
quit_timer:kill()
|
||||
if file.quit_timer then
|
||||
file.quit_timer:kill()
|
||||
end
|
||||
elseif not quit_timer then
|
||||
quit_timer = mp.add_timeout(quitseconds,
|
||||
elseif not file.quit_timer then
|
||||
file.quit_timer = mp.add_timeout(file.quitsec,
|
||||
function()
|
||||
stop()
|
||||
mp.command("quit")
|
||||
print("Quit after " .. opts.quit)
|
||||
end)
|
||||
else
|
||||
quit_timer["timeout"] = quitseconds
|
||||
quit_timer:kill()
|
||||
quit_timer:resume()
|
||||
file.quit_timer["timeout"] = file.quitsec
|
||||
file.quit_timer:kill()
|
||||
file.quit_timer:resume()
|
||||
end
|
||||
end
|
||||
autoquit()
|
||||
|
||||
local function fragment_chapters(packets, cache_time, stamp)
|
||||
local no_loop_chapters = get_chapters()
|
||||
local title = string.format("%s segment(s) dropped [%s]", packets, stamp)
|
||||
for _, chapter in ipairs(chapter_list) do
|
||||
if chapter["title"] == title then
|
||||
cache.packets[stamp]:kill()
|
||||
cache.packets[stamp] = nil
|
||||
return
|
||||
end
|
||||
end
|
||||
table.insert(chapter_list, {
|
||||
title = title,
|
||||
time = cache_time
|
||||
})
|
||||
if no_loop_chapters then
|
||||
mp.set_property_native("chapter-list", chapter_list)
|
||||
end
|
||||
end
|
||||
|
||||
local function packet_handler(t)
|
||||
if not opts.track_packets then -- second layer in case unregistering is async
|
||||
return
|
||||
end
|
||||
if t.prefix == "ffmpeg/demuxer" then
|
||||
local packets = t.text:match("^hls: skipping (%d+)")
|
||||
if packets then
|
||||
local cache_time = mp.get_property_number("demuxer-cache-time")
|
||||
if cache_time then
|
||||
-- ensure the chapters set
|
||||
cache.id = cache.id + 1
|
||||
local stamp = string.format("%#x", cache.id)
|
||||
cache.packets[stamp] = mp.add_periodic_timer(3,
|
||||
function()
|
||||
fragment_chapters(packets, cache_time, stamp)
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function packet_events(state)
|
||||
if not state then
|
||||
mp.unregister_event(packet_handler)
|
||||
for _, timer in pairs(cache.packets) do
|
||||
timer:kill()
|
||||
end
|
||||
cache.id = nil
|
||||
cache.packets = nil
|
||||
local no_loop_chapters = get_chapters()
|
||||
local n = #chapter_list
|
||||
for i = n, 1, -1 do
|
||||
if chapter_list[i]["title"]:match("%d+ segment%(s%) dropped") then
|
||||
table.remove(chapter_list, i)
|
||||
end
|
||||
end
|
||||
if no_loop_chapters and n > #chapter_list then
|
||||
mp.set_property_native("chapter-list", chapter_list)
|
||||
end
|
||||
else
|
||||
cache.id = 0
|
||||
cache.packets = {}
|
||||
mp.enable_messages("warn")
|
||||
mp.register_event("log-message", packet_handler)
|
||||
end
|
||||
end
|
||||
if opts.track_packets then
|
||||
packet_events(true)
|
||||
end
|
||||
|
||||
-- cache time observation switch for runtime changes
|
||||
function observe_cache()
|
||||
local network = mp.get_property_bool("demuxer-via-network")
|
||||
local obs_xyz = opts.autostart or cache.endsec or opts.hostchange
|
||||
local obs_xyz = opts.autostart or cache.endsec
|
||||
if not cache.observed and obs_xyz and network then
|
||||
cache.dumped = (file.pending or 0) ~= 0
|
||||
mp.observe_property("demuxer-cache-time", "number", automatic)
|
||||
cache.observed = true
|
||||
elseif (cache.observed or cache.dumped) and (not obs_xyz or not network) then
|
||||
|
@ -865,7 +1147,30 @@ function observe_cache()
|
|||
end
|
||||
end
|
||||
|
||||
mp.observe_property("media-uuid", "string", title_change)
|
||||
-- track-list observation switch for runtime changes
|
||||
function observe_tracks(state)
|
||||
if state then
|
||||
suspend()
|
||||
mp.observe_property("track-list", "native", detect)
|
||||
elseif state == false then
|
||||
mp.unobserve_property(detect)
|
||||
mp.unobserve_property(seamless)
|
||||
cache.prior = nil
|
||||
local timer = track.restart and track.restart:kill()
|
||||
-- reset the state on manual reloads
|
||||
elseif cache.prior then
|
||||
observe_tracks(false)
|
||||
observe_tracks(true)
|
||||
elseif opts.hostchange then
|
||||
suspend()
|
||||
end
|
||||
end
|
||||
|
||||
if opts.hostchange then
|
||||
observe_tracks(true)
|
||||
end
|
||||
|
||||
mp.observe_property("media-title", "string", title_change)
|
||||
|
||||
--[[ video and audio formats observed in order to handle track changes
|
||||
useful if e.g. --script-opts=ytdl_hook-all_formats=yes
|
||||
|
@ -878,13 +1183,16 @@ mp.observe_property("file-format", "string", container)
|
|||
an external file, so make sure existing chapters are not overwritten
|
||||
by observing A-B loop changes only after the file is loaded. ]]
|
||||
local function on_file_load()
|
||||
if file.loaded then
|
||||
chapter_points()
|
||||
else
|
||||
mp.observe_property("ab-loop-a", "native", chapter_points)
|
||||
mp.observe_property("ab-loop-b", "native", chapter_points)
|
||||
file.loaded = true
|
||||
end
|
||||
end
|
||||
mp.register_event("file-loaded", on_file_load)
|
||||
|
||||
mp.register_script_message("streamsave-rec", function() cache_write(opts.dump_mode)
|
||||
end)
|
||||
mp.register_script_message("streamsave-mode", mode_switch)
|
||||
mp.register_script_message("streamsave-title", title_override)
|
||||
mp.register_script_message("streamsave-extension", format_override)
|
||||
|
@ -896,6 +1204,12 @@ mp.register_script_message("streamsave-autoend", autoend_override)
|
|||
mp.register_script_message("streamsave-hostchange", hostchange_override)
|
||||
mp.register_script_message("streamsave-quit", quit_override)
|
||||
mp.register_script_message("streamsave-piecewise", piecewise_override)
|
||||
mp.register_script_message("streamsave-packets", packet_override)
|
||||
mp.register_script_message("streamsave-chapter",
|
||||
function(chapter)
|
||||
cache_write("chapter", _, tonumber(chapter))
|
||||
end
|
||||
)
|
||||
|
||||
mp.add_key_binding("Alt+z", "mode-switch", function() mode_switch("cycle") end)
|
||||
mp.add_key_binding("Ctrl+x", "stop-cache-write", stop)
|
||||
|
|
Loading…
Reference in New Issue