mirror of
http://gitea.phreedom.club/localhost_frssoft/funkwlmpv
synced 2024-11-22 07:21:28 +00: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
|
from shlex import quote
|
||||||
import mpv
|
import mpv
|
||||||
import time
|
import time
|
||||||
import sys
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
fzf = FzfPrompt()
|
fzf = FzfPrompt()
|
||||||
|
|
||||||
if get_config('enable_persistent_cache'):
|
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')
|
player.command('script-message', 'streamsave-path', 'cache')
|
||||||
else:
|
else:
|
||||||
player = mpv.MPV(cache=True, demuxer_max_bytes=25*1024*1024)
|
player = mpv.MPV(cache=True, demuxer_max_bytes=25*1024*1024)
|
||||||
|
|
|
@ -25,18 +25,22 @@ end
|
||||||
|
|
||||||
|
|
||||||
function make_cache_track(url)
|
function make_cache_track(url)
|
||||||
|
mp.command('script-message streamsave-autostart no')
|
||||||
find_uuid = "%x+-%x+-%x+-%x+-%x+"
|
find_uuid = "%x+-%x+-%x+-%x+-%x+"
|
||||||
uuid = string.sub(url, string.find(url, find_uuid))
|
uuid = string.sub(url, string.find(url, find_uuid))
|
||||||
host = get_url_host(url)
|
host = get_url_host(url)
|
||||||
cache_path_file = 'cache/' .. host .. '/' .. uuid .. '.mkv'
|
cache_path_file = 'cache/' .. host .. '/' .. uuid .. '.mkv'
|
||||||
|
cache_path_named_file = 'cache/' .. host .. '/' .. uuid .. '.mkv'
|
||||||
if false == file_exists(cache_path_file) then
|
if false == file_exists(cache_path_file) then
|
||||||
createDir('cache/' .. host .. '/')
|
createDir('cache/' .. host .. '/')
|
||||||
msg.verbose('Caching ' .. cache_path_file .. '')
|
msg.verbose('Caching ' .. cache_path_file .. '')
|
||||||
mp.command('script-message streamsave-title ' .. uuid .. '')
|
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.set_property('script-opts/media-uuid', uuid)
|
||||||
mp.command('script-message streamsave-extension .mkv')
|
mp.command('script-message streamsave-extension .mkv')
|
||||||
mp.command('script-message streamsave-path cache/' .. host .. '')
|
mp.command('script-message streamsave-path cache/' .. host .. '')
|
||||||
mp.command('script-message streamsave-rec')
|
mp.command('script-message streamsave-autostart yes')
|
||||||
else
|
else
|
||||||
msg.verbose('Already cached ' .. cache_path_file .. '')
|
msg.verbose('Already cached ' .. cache_path_file .. '')
|
||||||
os.execute('touch ' .. cache_path_file .. '')
|
os.execute('touch ' .. cache_path_file .. '')
|
||||||
|
@ -52,3 +56,11 @@ mp.add_hook("on_load", 11, function()
|
||||||
make_cache_track(url)
|
make_cache_track(url)
|
||||||
end
|
end
|
||||||
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
|
streamsave.lua
|
||||||
Version 0.20.6
|
Version 0.23.2
|
||||||
2022-10-13
|
2023-5-21
|
||||||
https://github.com/Sagnac/streamsave
|
https://github.com/Sagnac/streamsave
|
||||||
NOTE: Modified
|
|
||||||
|
|
||||||
mpv script aimed at saving live streams and clipping online videos without encoding.
|
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
|
* Automatic determination of the output file name and format
|
||||||
* Option to specify the preferred output directory
|
* 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
|
* Prevention of file overwrites
|
||||||
* Acceptance of inverted loop ranges, allowing the end point to be set first
|
* Acceptance of inverted loop ranges, allowing the end point to be set first
|
||||||
* Dynamic chapter indicators on the OSC displaying the clipping interval
|
* Dynamic chapter indicators on the OSC displaying the clipping interval
|
||||||
|
* Option to track HLS packet drops
|
||||||
* Automated stream saving
|
* Automated stream saving
|
||||||
* Workaround for some DAI HLS streams served from .m3u8 where the host changes
|
* 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=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 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.
|
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=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
|
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
|
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=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.
|
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).
|
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).
|
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.
|
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.
|
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:
|
Automation Options:
|
||||||
|
|
||||||
The autostart and autoend options are used for automated stream capturing.
|
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.
|
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.
|
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 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,
|
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.
|
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
|
-- change these in streamsave.conf
|
||||||
local opts = {
|
local opts = {
|
||||||
save_directory = [[.]], -- output file directory
|
save_directory = [[.]], -- output file directory
|
||||||
dump_mode = "continuous", -- <ab|current|continuous>
|
dump_mode = "ab", -- <ab|current|continuous|chapter|segments>
|
||||||
output_label = "overwrite", -- <increment|range|timestamp|overwrite>
|
output_label = "increment", -- <increment|range|timestamp|overwrite|chapter>
|
||||||
force_extension = ".mkv", -- <no|.ext> extension will be .ext if set
|
force_extension = "no", -- <no|.ext> extension will be .ext if set
|
||||||
force_title = "no", -- <no|title> custom title used for the filename
|
force_title = "no", -- <no|title> custom title used for the filename
|
||||||
range_marks = false, -- <yes|no> set chapters at A-B loop points?
|
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
|
autoend = "no", -- <no|HH:MM:SS> cache time to stop at
|
||||||
hostchange = false, -- <yes|no> use if the host changes mid stream
|
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
|
quit = "no", -- <no|HH:MM:SS> quits player at specified time
|
||||||
piecewise = false, -- <yes|no> writes stream in parts with autoend
|
piecewise = false, -- <yes|no> writes stream in parts with autoend
|
||||||
}
|
}
|
||||||
|
@ -141,8 +160,12 @@ local file = {
|
||||||
title, -- media title
|
title, -- media title
|
||||||
inc, -- filename increments
|
inc, -- filename increments
|
||||||
ext, -- file extension
|
ext, -- file extension
|
||||||
|
loaded, -- flagged once the initial load has taken place
|
||||||
pending, -- number of files pending write completion (max 2)
|
pending, -- number of files pending write completion (max 2)
|
||||||
queue, -- cache_write queue in case of multiple write requests
|
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
|
oldtitle, -- initialized if title is overridden, allows revert
|
||||||
oldext, -- initialized if format is overridden, allows revert
|
oldext, -- initialized if format is overridden, allows revert
|
||||||
oldpath, -- initialized if directory is overriden, 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
|
b_revert, -- B loop point prior to keyframe alignment
|
||||||
range, -- A-B loop range
|
range, -- A-B loop range
|
||||||
aligned, -- are the loop points aligned to keyframes?
|
aligned, -- are the loop points aligned to keyframes?
|
||||||
|
continuous, -- is the writing continuous?
|
||||||
}
|
}
|
||||||
|
|
||||||
local cache = {
|
local cache = {
|
||||||
dumped, -- autowrite cache state (serves as an autowrite request)
|
dumped, -- autowrite cache state (serves as an autowrite request)
|
||||||
observed, -- whether the cache time is being observed
|
observed, -- whether the cache time is being observed
|
||||||
endsec, -- user specified autoend cache time in seconds
|
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
|
seekend, -- seekable cache end timestamp
|
||||||
part, -- approx. end time of last piece / start time of next piece
|
part, -- approx. end time of last piece / start time of next piece
|
||||||
switch, -- request to observe track switches and seeking
|
switch, -- request to observe track switches and seeking
|
||||||
use, -- use cache_time instead of seekend for initial piece
|
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 track = {
|
||||||
local observe_cache
|
vid, -- video track id
|
||||||
local continuous
|
aid, -- audio track id
|
||||||
local write_file
|
sid, -- subtitle track id
|
||||||
local reset
|
restart, -- hostchange interval where subsequent reloads are immediate
|
||||||
local title_change
|
suspend, -- suspension interval on track-list changes
|
||||||
local container
|
}
|
||||||
|
|
||||||
|
local segments = {} -- chapter segments set for writing
|
||||||
local chapter_list = {} -- initial chapter list
|
local chapter_list = {} -- initial chapter list
|
||||||
local ab_chapters = {} -- A-B loop point chapters
|
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 title_change
|
||||||
local i, j, H, M, S = value:find("(%d+):(%d+):(%d+)")
|
local container
|
||||||
if not i then
|
local get_chapters
|
||||||
return
|
local chapter_points
|
||||||
else
|
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
|
return H*3600 + M*60 + S
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -199,16 +229,19 @@ local function validate_opts()
|
||||||
if opts.output_label ~= "increment" and
|
if opts.output_label ~= "increment" and
|
||||||
opts.output_label ~= "range" and
|
opts.output_label ~= "range" and
|
||||||
opts.output_label ~= "timestamp" and
|
opts.output_label ~= "timestamp" and
|
||||||
opts.output_label ~= "overwrite"
|
opts.output_label ~= "overwrite" and
|
||||||
|
opts.output_label ~= "chapter"
|
||||||
then
|
then
|
||||||
msg.warn("Invalid output_label '" .. opts.output_label .. "'")
|
msg.error("Invalid output_label '" .. opts.output_label .. "'")
|
||||||
opts.output_label = "increment"
|
opts.output_label = "increment"
|
||||||
end
|
end
|
||||||
if opts.dump_mode ~= "ab" and
|
if opts.dump_mode ~= "ab" and
|
||||||
opts.dump_mode ~= "current" 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
|
then
|
||||||
msg.warn("Invalid dump_mode '" .. opts.dump_mode .. "'")
|
msg.error("Invalid dump_mode '" .. opts.dump_mode .. "'")
|
||||||
opts.dump_mode = "ab"
|
opts.dump_mode = "ab"
|
||||||
end
|
end
|
||||||
if opts.autoend ~= "no" then
|
if opts.autoend ~= "no" then
|
||||||
|
@ -216,15 +249,15 @@ local function validate_opts()
|
||||||
cache.endsec = convert_time(opts.autoend)
|
cache.endsec = convert_time(opts.autoend)
|
||||||
end
|
end
|
||||||
if not convert_time(opts.autoend) then
|
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.")
|
"'. Use HH:MM:SS format.")
|
||||||
opts.autoend = "no"
|
opts.autoend = "no"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if opts.quit ~= "no" then
|
if opts.quit ~= "no" then
|
||||||
quitseconds = convert_time(opts.quit)
|
file.quitsec = convert_time(opts.quit)
|
||||||
if not quitseconds then
|
if not file.quitsec then
|
||||||
msg.warn("Invalid quit value '" .. opts.quit ..
|
msg.error("Invalid quit value '" .. opts.quit ..
|
||||||
"'. Use HH:MM:SS format.")
|
"'. Use HH:MM:SS format.")
|
||||||
opts.quit = "no"
|
opts.quit = "no"
|
||||||
end
|
end
|
||||||
|
@ -249,17 +282,22 @@ local function update_opts(changed)
|
||||||
if opts.range_marks then
|
if opts.range_marks then
|
||||||
chapter_points()
|
chapter_points()
|
||||||
else
|
else
|
||||||
ab_chapters = {}
|
if not get_chapters() then
|
||||||
mp.set_property_native("chapter-list", chapter_list)
|
mp.set_property_native("chapter-list", chapter_list)
|
||||||
end
|
end
|
||||||
|
ab_chapters = {}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
if changed["autoend"] then
|
if changed["autoend"] then
|
||||||
cache.endsec = convert_time(opts.autoend)
|
cache.endsec = convert_time(opts.autoend)
|
||||||
observe_cache()
|
observe_cache()
|
||||||
end
|
end
|
||||||
if changed["autostart"] or changed["hostchange"] then
|
if changed["autostart"] then
|
||||||
observe_cache()
|
observe_cache()
|
||||||
end
|
end
|
||||||
|
if changed["hostchange"] then
|
||||||
|
observe_tracks(opts.hostchange)
|
||||||
|
end
|
||||||
if changed["quit"] then
|
if changed["quit"] then
|
||||||
autoquit()
|
autoquit()
|
||||||
end
|
end
|
||||||
|
@ -268,6 +306,9 @@ local function update_opts(changed)
|
||||||
elseif changed["piecewise"] then
|
elseif changed["piecewise"] then
|
||||||
cache.endsec = convert_time(opts.autoend)
|
cache.endsec = convert_time(opts.autoend)
|
||||||
end
|
end
|
||||||
|
if changed["track_packets"] then
|
||||||
|
packet_events(opts.track_packets)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
options.read_options(opts, "streamsave", update_opts)
|
options.read_options(opts, "streamsave", update_opts)
|
||||||
|
@ -281,6 +322,10 @@ local function mode_switch(value)
|
||||||
value = "current"
|
value = "current"
|
||||||
elseif opts.dump_mode == "current" then
|
elseif opts.dump_mode == "current" then
|
||||||
value = "continuous"
|
value = "continuous"
|
||||||
|
elseif opts.dump_mode == "continuous" then
|
||||||
|
value = "chapter"
|
||||||
|
elseif opts.dump_mode == "chapter" then
|
||||||
|
value = "segments"
|
||||||
else
|
else
|
||||||
value = "ab"
|
value = "ab"
|
||||||
end
|
end
|
||||||
|
@ -297,8 +342,16 @@ local function mode_switch(value)
|
||||||
opts.dump_mode = "current"
|
opts.dump_mode = "current"
|
||||||
print("Current position mode")
|
print("Current position mode")
|
||||||
mp.osd_message("Cache write mode: Current position")
|
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
|
else
|
||||||
msg.warn("Invalid dump mode '" .. value .. "'")
|
msg.error("Invalid dump mode '" .. value .. "'")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -321,12 +374,13 @@ function container(_, _, req)
|
||||||
local file_format = mp.get_property("file-format")
|
local file_format = mp.get_property("file-format")
|
||||||
if not file_format then
|
if not file_format then
|
||||||
reset()
|
reset()
|
||||||
|
observe_tracks()
|
||||||
return end
|
return end
|
||||||
if opts.force_extension ~= "no" and not req then
|
if opts.force_extension ~= "no" and not req then
|
||||||
file.ext = opts.force_extension
|
file.ext = opts.force_extension
|
||||||
observe_cache()
|
observe_cache()
|
||||||
return end
|
return end
|
||||||
if string.find(file_format, "mp4")
|
if string.match(file_format, "mp4")
|
||||||
or ((video == "h264" or video == "av1" or not video) and
|
or ((video == "h264" or video == "av1" or not video) and
|
||||||
(audio == "aac" or not audio))
|
(audio == "aac" or not audio))
|
||||||
then
|
then
|
||||||
|
@ -339,6 +393,7 @@ function container(_, _, req)
|
||||||
file.ext = ".mkv"
|
file.ext = ".mkv"
|
||||||
end
|
end
|
||||||
observe_cache()
|
observe_cache()
|
||||||
|
observe_tracks()
|
||||||
file.oldext = nil
|
file.oldext = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -395,6 +450,8 @@ local function label_override(value)
|
||||||
value = "timestamp"
|
value = "timestamp"
|
||||||
elseif opts.output_label == "timestamp" then
|
elseif opts.output_label == "timestamp" then
|
||||||
value = "overwrite"
|
value = "overwrite"
|
||||||
|
elseif opts.output_label == "overwrite" then
|
||||||
|
value = "chapter"
|
||||||
else
|
else
|
||||||
value = "increment"
|
value = "increment"
|
||||||
end
|
end
|
||||||
|
@ -408,8 +465,10 @@ end
|
||||||
local function marks_override(value)
|
local function marks_override(value)
|
||||||
if not value or value == "no" then
|
if not value or value == "no" then
|
||||||
opts.range_marks = false
|
opts.range_marks = false
|
||||||
ab_chapters = {}
|
if not get_chapters() then
|
||||||
mp.set_property_native("chapter-list", chapter_list)
|
mp.set_property_native("chapter-list", chapter_list)
|
||||||
|
end
|
||||||
|
ab_chapters = {}
|
||||||
print("Range marks disabled")
|
print("Range marks disabled")
|
||||||
mp.osd_message("streamsave: range marks disabled")
|
mp.osd_message("streamsave: range marks disabled")
|
||||||
elseif value == "yes" then
|
elseif value == "yes" then
|
||||||
|
@ -418,17 +477,12 @@ local function marks_override(value)
|
||||||
print("Range marks enabled")
|
print("Range marks enabled")
|
||||||
mp.osd_message("streamsave: range marks enabled")
|
mp.osd_message("streamsave: range marks enabled")
|
||||||
else
|
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")
|
mp.osd_message("streamsave: invalid input; use yes or no")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function autostart_override(value)
|
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
|
if not value or value == "no" then
|
||||||
opts.autostart = false
|
opts.autostart = false
|
||||||
print("Autostart disabled")
|
print("Autostart disabled")
|
||||||
|
@ -437,6 +491,10 @@ local function autostart_override(value)
|
||||||
opts.autostart = true
|
opts.autostart = true
|
||||||
print("Autostart enabled")
|
print("Autostart enabled")
|
||||||
mp.osd_message("streamsave: 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
|
end
|
||||||
observe_cache()
|
observe_cache()
|
||||||
end
|
end
|
||||||
|
@ -451,24 +509,31 @@ local function autoend_override(value)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function hostchange_override(value)
|
local function hostchange_override(value)
|
||||||
|
local hostchange = opts.hostchange
|
||||||
value = value == "cycle" and (not opts.hostchange and "yes" or "no") or value
|
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
|
if not value or value == "no" then
|
||||||
opts.hostchange = false
|
opts.hostchange = false
|
||||||
mp.unobserve_property(reload)
|
|
||||||
local timer = cache.restart and cache.restart:kill()
|
|
||||||
print("Hostchange disabled")
|
print("Hostchange disabled")
|
||||||
mp.osd_message("streamsave: hostchange disabled")
|
mp.osd_message("streamsave: hostchange disabled")
|
||||||
elseif value == "yes" then
|
elseif value == "yes" then
|
||||||
opts.hostchange = true
|
opts.hostchange = true
|
||||||
print("Hostchange enabled")
|
print("Hostchange enabled")
|
||||||
mp.osd_message("streamsave: 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
|
end
|
||||||
observe_cache()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function quit_override(value)
|
local function quit_override(value)
|
||||||
|
@ -491,11 +556,33 @@ local function piecewise_override(value)
|
||||||
print("Piecewise dumping enabled")
|
print("Piecewise dumping enabled")
|
||||||
mp.osd_message("streamsave: piecewise dumping enabled")
|
mp.osd_message("streamsave: piecewise dumping enabled")
|
||||||
else
|
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")
|
mp.osd_message("streamsave: invalid input; use yes or no")
|
||||||
end
|
end
|
||||||
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()
|
local function range_flip()
|
||||||
loop.a = mp.get_property_number("ab-loop-a")
|
loop.a = mp.get_property_number("ab-loop-a")
|
||||||
loop.b = mp.get_property_number("ab-loop-b")
|
loop.b = mp.get_property_number("ab-loop-b")
|
||||||
|
@ -550,6 +637,47 @@ local function range_stamp(mode)
|
||||||
end
|
end
|
||||||
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 function write_set(mode, file_name, file_pos, quiet)
|
||||||
local command = {
|
local command = {
|
||||||
_flags = {
|
_flags = {
|
||||||
|
@ -559,6 +687,11 @@ local function write_set(mode, file_name, file_pos, quiet)
|
||||||
}
|
}
|
||||||
if mode == "ab" then
|
if mode == "ab" then
|
||||||
command["name"] = "ab-loop-dump-cache"
|
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
|
else
|
||||||
command["name"] = "dump-cache"
|
command["name"] = "dump-cache"
|
||||||
command["start"] = 0
|
command["start"] = 0
|
||||||
|
@ -567,18 +700,67 @@ local function write_set(mode, file_name, file_pos, quiet)
|
||||||
return command
|
return command
|
||||||
end
|
end
|
||||||
|
|
||||||
local function cache_write(mode, quiet)
|
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
|
||||||
|
if success then
|
||||||
|
print("Finished writing cache to: " .. file_name)
|
||||||
|
else
|
||||||
|
msg.warn("Possibly broken file created at: " .. file_name)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
msg.error("File not written.")
|
||||||
|
end
|
||||||
|
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 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
|
if not (file.title and file.ext) then
|
||||||
return end
|
return end
|
||||||
if file.pending == 2 then
|
if file.pending == 2
|
||||||
|
or segments[1] and file.pending > 0 and not loop.continuous
|
||||||
|
then
|
||||||
file.queue = file.queue or {}
|
file.queue = file.queue or {}
|
||||||
-- honor extra write requests when pending queue is full
|
-- honor extra write requests when pending queue is full
|
||||||
-- but limit number of outstanding write requests to be fulfilled
|
-- but limit number of outstanding write requests to be fulfilled
|
||||||
if #file.queue < 10 then
|
if #file.queue < 10 then
|
||||||
table.insert(file.queue, {mode, quiet})
|
table.insert(file.queue, {mode, quiet, chapter})
|
||||||
end
|
end
|
||||||
return end
|
return end
|
||||||
range_flip()
|
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
|
-- evaluate tagging conditions and set file name
|
||||||
if opts.output_label == "increment" then
|
if opts.output_label == "increment" then
|
||||||
increment_filename()
|
increment_filename()
|
||||||
|
@ -588,42 +770,27 @@ local function cache_write(mode, quiet)
|
||||||
file.name = set_name(-os.time())
|
file.name = set_name(-os.time())
|
||||||
elseif opts.output_label == "overwrite" then
|
elseif opts.output_label == "overwrite" then
|
||||||
file.name = set_name("")
|
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
|
end
|
||||||
-- dump cache according to mode
|
-- dump cache according to mode
|
||||||
local file_pos
|
local file_pos
|
||||||
local file_name = file.name -- scope reduction so callback verifies correct file
|
|
||||||
file.pending = (file.pending or 0) + 1
|
file.pending = (file.pending or 0) + 1
|
||||||
continuous = mode == "continuous" or loop.a and not loop.b
|
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
|
if mode == "current" then
|
||||||
file_pos = mp.get_property_number("playback-time", 0)
|
file_pos = mp.get_property_number("playback-time", 0)
|
||||||
elseif continuous and file.pending == 1 then
|
elseif loop.continuous and file.pending == 1 then
|
||||||
print("Dumping cache continuously to:" .. file_name)
|
print("Dumping cache continuously to: " .. file.name)
|
||||||
end
|
end
|
||||||
write_file = mp.command_native_async (
|
local commands = write_set(mode, file.name, file_pos, quiet)
|
||||||
write_set(mode, file_name, file_pos, quiet),
|
local callback = on_write_finish(cache_write, mode, file.name)
|
||||||
function(success, _, command_error)
|
file.writing = mp.command_native_async(commands, callback)
|
||||||
command_error = command_error and msg.error(command_error)
|
|
||||||
-- check if file is written
|
|
||||||
if utils.file_info(file_name) then
|
|
||||||
if success then
|
|
||||||
print("Finished writing cache to:" .. file_name)
|
|
||||||
else
|
|
||||||
msg.warn("Possibly broken file created at:" .. file_name)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
msg.error("File not written.")
|
|
||||||
end
|
|
||||||
if 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])
|
|
||||||
table.remove(file.queue, 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
)
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -649,16 +816,15 @@ local function align_cache()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- creates chapters at A-B loop points
|
function get_chapters()
|
||||||
function chapter_points()
|
|
||||||
if not opts.range_marks then
|
|
||||||
return end
|
|
||||||
local current_chapters = mp.get_property_native("chapter-list", {})
|
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
|
-- 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$")
|
not string.match(current_chapters[1]["title"], "^[AB] loop point$")
|
||||||
then
|
then
|
||||||
chapter_list = current_chapters
|
chapter_list = current_chapters
|
||||||
|
updated = true
|
||||||
-- if a script has added chapters after A-B points are set then
|
-- if a script has added chapters after A-B points are set then
|
||||||
-- add those to the original chapter list
|
-- add those to the original chapter list
|
||||||
elseif #current_chapters > #ab_chapters then
|
elseif #current_chapters > #ab_chapters then
|
||||||
|
@ -666,12 +832,22 @@ function chapter_points()
|
||||||
table.insert(chapter_list, current_chapters[i])
|
table.insert(chapter_list, current_chapters[i])
|
||||||
end
|
end
|
||||||
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 = {}
|
ab_chapters = {}
|
||||||
-- restore original chapter list if A-B points are cleared
|
-- restore original chapter list if A-B points are cleared
|
||||||
-- otherwise set chapters to A-B points
|
-- otherwise set chapters to A-B points
|
||||||
range_flip()
|
range_flip()
|
||||||
if not loop.a and not loop.b then
|
if not loop.a and not loop.b then
|
||||||
|
if not updated then
|
||||||
mp.set_property_native("chapter-list", chapter_list)
|
mp.set_property_native("chapter-list", chapter_list)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
if loop.a then
|
if loop.a then
|
||||||
ab_chapters[1] = {
|
ab_chapters[1] = {
|
||||||
|
@ -696,36 +872,53 @@ end
|
||||||
|
|
||||||
-- stops writing the file
|
-- stops writing the file
|
||||||
local function stop()
|
local function stop()
|
||||||
mp.abort_async_command(write_file or {})
|
mp.abort_async_command(file.writing or {})
|
||||||
end
|
end
|
||||||
|
|
||||||
function reset()
|
function reset()
|
||||||
if cache.observed or cache.dumped then
|
if cache.observed or cache.dumped then
|
||||||
stop()
|
stop()
|
||||||
mp.unobserve_property(automatic)
|
mp.unobserve_property(automatic)
|
||||||
mp.unobserve_property(reload)
|
|
||||||
mp.unobserve_property(get_seekable_cache)
|
mp.unobserve_property(get_seekable_cache)
|
||||||
cache.endsec = convert_time(opts.autoend)
|
cache.endsec = convert_time(opts.autoend)
|
||||||
cache.observed = false
|
cache.observed = false
|
||||||
end
|
end
|
||||||
cache.prior = 0
|
|
||||||
cache.part = 0
|
cache.part = 0
|
||||||
cache.dumped = false
|
cache.dumped = false
|
||||||
cache.switch = true
|
cache.switch = true
|
||||||
end
|
end
|
||||||
reset()
|
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
|
-- use the seekable part of the cache for more accurate timestamps
|
||||||
local cache_state = mp.get_property_native("demuxer-cache-state", {})
|
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 {}
|
local seekable_ranges = cache_state["seekable-ranges"] or {}
|
||||||
if prop then
|
if prop then
|
||||||
if range_check ~= false and
|
if range_check ~= false and
|
||||||
(#seekable_ranges == 0
|
(#seekable_ranges == 0
|
||||||
or not mp.get_property_number("demuxer-cache-time"))
|
or not cache_state["cache-end"])
|
||||||
then
|
then
|
||||||
reset()
|
reset()
|
||||||
cache.use = opts.piecewise
|
cache.use = opts.piecewise
|
||||||
|
@ -737,45 +930,67 @@ function get_seekable_cache(prop, range_check, underrun)
|
||||||
for i, range in ipairs(seekable_ranges) do
|
for i, range in ipairs(seekable_ranges) do
|
||||||
seekable_ends[i] = range["end"] or 0
|
seekable_ends[i] = range["end"] or 0
|
||||||
end
|
end
|
||||||
cache.seekend = math.max(0, unpack(seekable_ends))
|
return math.max(0, unpack(seekable_ends))
|
||||||
return cache.seekend
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function reload(_, play_time)
|
-- seamlessly reload on inserts (hostchange)
|
||||||
local cache_duration = mp.get_property_number("demuxer-cache-duration")
|
local function seamless(_, cache_state)
|
||||||
if play_time and play_time >= cache.seekend - 0.25
|
cache_state = cache_state or {}
|
||||||
or cache_duration and math.abs(cache.prior - cache_duration) > 4800
|
local reader = math.abs(cache_state["reader-pts"] or 0)
|
||||||
or get_seekable_cache(nil, false, true)
|
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
|
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()
|
reset()
|
||||||
cache.restart = cache.restart or mp.add_timeout(300, function() end)
|
observe_tracks(false)
|
||||||
cache.restart:resume()
|
cache.observed = true
|
||||||
msg.warn("Reloading stream due to host change.")
|
cache.prior = math.abs(mp.get_property_number("demuxer-cache-duration", 4E3))
|
||||||
mp.command("playlist-play-index current")
|
cache.seekend = get_seekable_cache()
|
||||||
|
mp.observe_property("demuxer-cache-state", "native", seamless)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function automatic(_, cache_time)
|
function automatic(_, cache_time)
|
||||||
if opts.hostchange and cache.prior ~= 0
|
if not cache_time then
|
||||||
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
|
|
||||||
reset()
|
reset()
|
||||||
cache.use = opts.piecewise
|
cache.use = opts.piecewise
|
||||||
observe_cache()
|
observe_cache()
|
||||||
|
@ -805,12 +1020,9 @@ function automatic(_, cache_time)
|
||||||
mp.observe_property("seeking", "bool", get_seekable_cache)
|
mp.observe_property("seeking", "bool", get_seekable_cache)
|
||||||
end
|
end
|
||||||
-- unobserve cache time if not needed
|
-- unobserve cache time if not needed
|
||||||
if cache.dumped and not cache.switch
|
if cache.dumped and not cache.switch and not cache.endsec then
|
||||||
and not cache.endsec and not opts.hostchange
|
|
||||||
then
|
|
||||||
mp.unobserve_property(automatic)
|
mp.unobserve_property(automatic)
|
||||||
cache.observed = false
|
cache.observed = false
|
||||||
cache.prior = 0
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
-- stop cache dump
|
-- stop cache dump
|
||||||
|
@ -830,34 +1042,104 @@ function automatic(_, cache_time)
|
||||||
end
|
end
|
||||||
stop()
|
stop()
|
||||||
end
|
end
|
||||||
cache.prior = cache_time
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function autoquit()
|
function autoquit()
|
||||||
if opts.quit == "no" then
|
if opts.quit == "no" then
|
||||||
if quit_timer then
|
if file.quit_timer then
|
||||||
quit_timer:kill()
|
file.quit_timer:kill()
|
||||||
end
|
end
|
||||||
elseif not quit_timer then
|
elseif not file.quit_timer then
|
||||||
quit_timer = mp.add_timeout(quitseconds,
|
file.quit_timer = mp.add_timeout(file.quitsec,
|
||||||
function()
|
function()
|
||||||
stop()
|
stop()
|
||||||
mp.command("quit")
|
mp.command("quit")
|
||||||
print("Quit after " .. opts.quit)
|
print("Quit after " .. opts.quit)
|
||||||
end)
|
end)
|
||||||
else
|
else
|
||||||
quit_timer["timeout"] = quitseconds
|
file.quit_timer["timeout"] = file.quitsec
|
||||||
quit_timer:kill()
|
file.quit_timer:kill()
|
||||||
quit_timer:resume()
|
file.quit_timer:resume()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
autoquit()
|
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
|
-- cache time observation switch for runtime changes
|
||||||
function observe_cache()
|
function observe_cache()
|
||||||
local network = mp.get_property_bool("demuxer-via-network")
|
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
|
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)
|
mp.observe_property("demuxer-cache-time", "number", automatic)
|
||||||
cache.observed = true
|
cache.observed = true
|
||||||
elseif (cache.observed or cache.dumped) and (not obs_xyz or not network) then
|
elseif (cache.observed or cache.dumped) and (not obs_xyz or not network) then
|
||||||
|
@ -865,7 +1147,30 @@ function observe_cache()
|
||||||
end
|
end
|
||||||
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
|
--[[ video and audio formats observed in order to handle track changes
|
||||||
useful if e.g. --script-opts=ytdl_hook-all_formats=yes
|
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
|
an external file, so make sure existing chapters are not overwritten
|
||||||
by observing A-B loop changes only after the file is loaded. ]]
|
by observing A-B loop changes only after the file is loaded. ]]
|
||||||
local function on_file_load()
|
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-a", "native", chapter_points)
|
||||||
mp.observe_property("ab-loop-b", "native", chapter_points)
|
mp.observe_property("ab-loop-b", "native", chapter_points)
|
||||||
|
file.loaded = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
mp.register_event("file-loaded", on_file_load)
|
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-mode", mode_switch)
|
||||||
mp.register_script_message("streamsave-title", title_override)
|
mp.register_script_message("streamsave-title", title_override)
|
||||||
mp.register_script_message("streamsave-extension", format_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-hostchange", hostchange_override)
|
||||||
mp.register_script_message("streamsave-quit", quit_override)
|
mp.register_script_message("streamsave-quit", quit_override)
|
||||||
mp.register_script_message("streamsave-piecewise", piecewise_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("Alt+z", "mode-switch", function() mode_switch("cycle") end)
|
||||||
mp.add_key_binding("Ctrl+x", "stop-cache-write", stop)
|
mp.add_key_binding("Ctrl+x", "stop-cache-write", stop)
|
||||||
|
|
Loading…
Reference in a new issue