parent
ee703bca50
commit
1a3f23efe4
|
@ -1,6 +1,2 @@
|
|||
.DS_Store
|
||||
test_video*
|
||||
config.lua
|
||||
make_cuts
|
||||
node_modules
|
||||
dist/
|
||||
|
|
337
README.md
337
README.md
|
@ -1,33 +1,37 @@
|
|||
# mpv-cut
|
||||
|
||||
## What Is This
|
||||
This extension allows you to:
|
||||
|
||||
The core functionality of this script is to very
|
||||
quickly cut videos both losslessly and re-encoded-ly
|
||||
with the help of the fantastic media player
|
||||
[mpv](https://mpv.io/installation/).
|
||||
- Quickly cut videos both losslessly and re-encoded-ly.
|
||||
|
||||
There is also the added functionality of:
|
||||
- Logging the timestamps of cuts to a text file for later use with the `make_cuts` script.
|
||||
- Logging the current timestamp to a bookmark file and reloading them as chapters in mpv.
|
||||
- Specify custom actions in a `config.lua` file to support your own use cases
|
||||
without having to modify the script itself or write your own extension.
|
||||
|
||||
More details in [usage](#usage).
|
||||
- Bookmark timestamps to a `.book` file and load them as chapters.
|
||||
|
||||
- Save cut information to a `.list` file for backup and make cuts later.
|
||||
|
||||
- Choose meaningful channel names to organize the aforementioned actions.
|
||||
|
||||
All directly in the fantastic media player [mpv](https://mpv.io/installation/).
|
||||
|
||||
## Requirements
|
||||
Besides mpv, you must have the following in your PATH:
|
||||
- ffmpeg
|
||||
- node
|
||||
|
||||
Besides mpv, you must have `ffmpeg` in your PATH.
|
||||
|
||||
## Installation
|
||||
|
||||
#### Linux/MacOS
|
||||
|
||||
```
|
||||
git clone -b release --single-branch "https://github.com/familyfriendlymikey/mpv-cut.git" ~/.config/mpv/scripts/mpv-cut
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
In
|
||||
`%AppData%\Roaming\mpv\scripts` or `Users\user\scoop\persist\mpv\scripts` run:
|
||||
|
||||
```
|
||||
git clone -b release --single-branch "https://github.com/familyfriendlymikey/mpv-cut.git"
|
||||
```
|
||||
|
@ -39,107 +43,112 @@ That's all you have to do, next time you run mpv the script will be automaticall
|
|||
### Cutting A Video Losslessly
|
||||
|
||||
- Press `c` to begin a cut.
|
||||
|
||||
- Seek to a later time in the video.
|
||||
|
||||
- Press `c` again to make the cut.
|
||||
|
||||
The resulting cut will be placed in the same directory as the source file.
|
||||
|
||||
### Other Actions
|
||||
### Actions
|
||||
|
||||
You can press `a` to cycle between the two default actions:
|
||||
You can press `a` to cycle between three default actions:
|
||||
|
||||
- Copy (lossless cut, rounds to keyframes).
|
||||
|
||||
- Encode (re-encoded cut, exact).
|
||||
|
||||
### Cutting To Global Dir
|
||||
- List (simply add the timestamps for the cut to a `.list` file).
|
||||
|
||||
You can press `g` to toggle saving to the
|
||||
configured global directory as opposed to
|
||||
the same directory as the source file.
|
||||
|
||||
### Cut Lists
|
||||
Instead of making cuts immediately, you can choose to store all cuts
|
||||
in a "cut list" text file.
|
||||
Toggle this behavior with the `l` key.
|
||||
When you're ready to make your cuts, press `0` in mpv.
|
||||
|
||||
The `make_cuts` script can also be invoked directly.
|
||||
The very last argument must be rows of JSON.
|
||||
In the case of a cut list, you can simply do
|
||||
```
|
||||
./make_cuts "$(cat cut_list_name.list)"
|
||||
```
|
||||
Preceding the aforementioned JSON argument may be 0 to 2 arguments:
|
||||
- If there are `0` arguments,
|
||||
it will use the current working directory
|
||||
as the input dir to look for the filenames of the cuts passed
|
||||
to it, and will also output cuts to the current directory.
|
||||
- If there is `1` argument, it will use it
|
||||
as both the indir and outdir.
|
||||
- If there are `2` arguments, it will use the first one
|
||||
as the indir and the second one as the outdir.
|
||||
|
||||
If any of the filenames do not exist in the indir,
|
||||
the ffmpeg command for that cut will simply fail.
|
||||
`mpv-cut` uses an extensible list of *actions* that you can modify in your
|
||||
`config.lua`. This makes it easy to change the `ffmpeg` command (or any command
|
||||
for that matter) to suit your specific situation. It should be possible to
|
||||
create custom actions with limited coding knowledge. More details in
|
||||
[config](#config).
|
||||
|
||||
### Bookmarking
|
||||
|
||||
Press `i` to append the current timestamp to a bookmark text file.
|
||||
This automatically reloads the timestamps as chapters in mpv.
|
||||
You can navigate between these chapters with the default mpv bindings,
|
||||
`!` and `@`.
|
||||
Press `i` to append the current timestamp to a `.book` file. This
|
||||
automatically reloads the timestamps as chapters in mpv. You can navigate
|
||||
between these chapters with the default mpv bindings, `!` and `@`.
|
||||
|
||||
### Channels
|
||||
|
||||
The resulting cuts and bookmark files will be prefixed a channel
|
||||
number. This is to help you categorize cuts and bookmarks. You
|
||||
can press `-` to decrement the channel and `=` to increment the
|
||||
channel.
|
||||
The resulting cuts and bookmark files will be prefixed a channel number. This
|
||||
is to help you categorize cuts and bookmarks. You can press `-` to decrement
|
||||
the channel and `=` to increment the channel.
|
||||
|
||||
You can configure a name for each channel as shown below.
|
||||
|
||||
### Making Cuts
|
||||
|
||||
If you want to make all the cuts stored in a cut list, simply press `0`.
|
||||
|
||||
## Config
|
||||
|
||||
You can configure settings by creating a `config.lua` file
|
||||
in the same directory as `main.lua`.
|
||||
You can configure settings by creating a `config.lua` file in the same
|
||||
directory as `main.lua`.
|
||||
|
||||
You can include or omit any of the following:
|
||||
|
||||
```lua
|
||||
-- Set to true if you want to use the global dir by default.
|
||||
USE_GLOBAL_DIR = true
|
||||
|
||||
-- Configure your global directory
|
||||
GLOBAL_DIR = "~/Desktop"
|
||||
|
||||
-- Set to true if you want to use cut lists by default.
|
||||
USE_CUT_LIST = false
|
||||
|
||||
-- The list of actions to cycle through, in order.
|
||||
ACTIONS = { "COPY", "ENCODE" }
|
||||
-- Key config
|
||||
KEY_CUT = "c"
|
||||
KEY_CYCLE_ACTION = "a"
|
||||
KEY_BOOKMARK_ADD = "i"
|
||||
KEY_CHANNEL_INC = "="
|
||||
KEY_CHANNEL_DEC = "-"
|
||||
KEY_MAKE_CUTS = "0"
|
||||
|
||||
-- The list of channel names, you can choose whatever you want.
|
||||
CHANNEL_NAMES = {}
|
||||
CHANNEL_NAMES[1] = "FUNNY"
|
||||
CHANNEL_NAMES[2] = "COOL"
|
||||
|
||||
-- The default channel
|
||||
CHANNEL = 1
|
||||
|
||||
-- Key config
|
||||
KEY_CUT = "c"
|
||||
KEY_MAKE_CUTS = "0"
|
||||
KEY_CYCLE_ACTION = "a"
|
||||
KEY_TOGGLE_USE_GLOBAL_DIR = "g"
|
||||
KEY_TOGGLE_USE_CUT_LIST = "l"
|
||||
KEY_BOOKMARK_ADD = "i"
|
||||
KEY_CHANNEL_INC = "="
|
||||
KEY_CHANNEL_DEC = "-"
|
||||
-- The default action
|
||||
ACTION = ACTIONS.ENCODE
|
||||
|
||||
-- The action to use when making cuts from a cut list
|
||||
MAKE_CUT = ACTIONS.COPY
|
||||
|
||||
-- Delete a default action
|
||||
ACTIONS.LIST = nil
|
||||
|
||||
-- Specify custom actions
|
||||
ACTIONS.ENCODE = function(d)
|
||||
local args = {
|
||||
"ffmpeg",
|
||||
"-nostdin", "-y",
|
||||
"-loglevel", "error",
|
||||
"-i", d.inpath,
|
||||
"-ss", d.start_time,
|
||||
"-t", d.duration,
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-crf", "16",
|
||||
"-preset", "superfast",
|
||||
utils.join_path(d.indir, "ENCODE_" .. d.channel .. "_" .. d.infile_noext .. "_FROM_" .. d.start_time_hms .. "_TO_" .. d.end_time_hms .. d.ext)
|
||||
}
|
||||
mp.command_native_async({
|
||||
name = "subprocess",
|
||||
args = args,
|
||||
playback_only = false,
|
||||
}, function() print("Done") end)
|
||||
end
|
||||
|
||||
-- The table that gets passed to an action will have the following properties:
|
||||
-- inpath, indir, infile, infile_noext, ext
|
||||
-- channel
|
||||
-- start_time, end_time, duration
|
||||
-- start_time_hms, end_time_hms, duration_hms
|
||||
```
|
||||
|
||||
## Optimized MPV Input Config
|
||||
|
||||
This is my `input.conf` file, and it is optimized
|
||||
for both normal playback and quickly editing videos.
|
||||
This is my `input.conf` file, and it is optimized for both normal playback and
|
||||
quickly editing videos.
|
||||
|
||||
```
|
||||
RIGHT seek 2 exact
|
||||
LEFT seek -2 exact
|
||||
|
@ -155,165 +164,137 @@ UP seek 0.01 keyframes # Seek by keyframes only.
|
|||
DOWN seek -0.01 keyframes # Seek by keyframes only.
|
||||
```
|
||||
|
||||
You may also want to change your key repeat delay and rate
|
||||
by tweaking `input-ar-delay` and `input-ar-rate`
|
||||
to your liking in `mpv.conf`.
|
||||
You may also want to change your key repeat delay and rate by tweaking
|
||||
`input-ar-delay` and `input-ar-rate` to your liking in `mpv.conf`.
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why Do I Need Node?
|
||||
|
||||
The actual `mpv-cut` extension acts as a sort of minimal interface to an
|
||||
arbitrary `make_cuts` binary. This way, users can extend the
|
||||
functionality using whatever language they want, without being tied to
|
||||
LUA and relying on `mpv`'s API. Most people on GitHub know how to code
|
||||
in some language, but not everyone wants to learn LUA and an API to
|
||||
cut their videos.
|
||||
|
||||
I chose to write the default `make_cuts` script in Imba, an extremely
|
||||
underrated language that compiles to readable JavaScript. Python would
|
||||
have been a good candidate as well, but Python's VM takes
|
||||
significantly longer to start up than Node's which I didn't like.
|
||||
|
||||
### What Is The Point Of A Cut List?
|
||||
|
||||
There are plenty of reasons, but to give some examples:
|
||||
|
||||
- Video seems to be pretty complex, at least to me.
|
||||
One video file may cause certain issues,
|
||||
and another may not, which makes writing
|
||||
an ffmpeg command that accounts for all scenarios difficult.
|
||||
If you spend a ton of time making many cuts in a long movie
|
||||
only to find that the colors look off because of some
|
||||
10-bit h265 dolby mega surround whatever the fuck,
|
||||
with a cut list it's trivial to edit
|
||||
the ffmpeg command and re-make the cuts.
|
||||
- Video seems to be pretty complex, at least to me. One video file may cause
|
||||
certain issues, and another may not, which makes writing an ffmpeg command
|
||||
that accounts for all scenarios difficult. If you spend a ton of time making
|
||||
many cuts in a long movie only to find that the colors look off because of
|
||||
some 10-bit h265 dolby mega surround whatever the fuck, with a cut list it's
|
||||
trivial to edit the ffmpeg command and re-make the cuts.
|
||||
|
||||
- Maybe you forget that the foreign language video you're cutting has
|
||||
softsubs rather than hardsubs,
|
||||
and you make a bunch of encode cuts
|
||||
resulting in cuts that have no subtitles.
|
||||
- Maybe you forget that the foreign language video you're cutting has softsubs
|
||||
rather than hardsubs, and you make a bunch of encode cuts resulting in cuts
|
||||
that have no subtitles.
|
||||
|
||||
- You delete the source video for storage reasons,
|
||||
but still want to have a back up of the cut timestamps
|
||||
in the event you need to remake the cuts.
|
||||
- You delete the source video for storage reasons, but still want to have a
|
||||
back up of the cut timestamps in the event you need to remake the cuts.
|
||||
|
||||
### Why Would I Bookmark Instead Of Cutting?
|
||||
|
||||
Suppose you're watching a movie or show for your own enjoyment,
|
||||
but you also want to compile funny moments to post online
|
||||
or send to your friends.
|
||||
It would ruin your viewing experience to wait for a funny
|
||||
moment to be over in order to make a cut.
|
||||
Instead, you can quickly make a bookmark whenever you laugh,
|
||||
and once you're done watching you can go back and make actual cuts.
|
||||
Suppose you're watching a movie or show for your own enjoyment, but you also
|
||||
want to compile funny moments to post online or send to your friends. It would
|
||||
ruin your viewing experience to wait for a funny moment to be over in order to
|
||||
make a cut. Instead, you can quickly make a bookmark whenever you laugh, and
|
||||
once you're done watching you can go back and make actual cuts.
|
||||
|
||||
### Why Is Lossless Cutting Called "Copy"?
|
||||
This refers to ffmpeg's `-copy` flag which copies the input stream
|
||||
instead of re-encoding it, meaning that the cut will process
|
||||
extremely quickly and the resulting video will retain
|
||||
100% of the original quality.
|
||||
The main drawback is that the cut may have some extra
|
||||
video at the beginning and end,
|
||||
and as a result of that there may be some slightly wonky behavior
|
||||
with video players and editors.
|
||||
|
||||
This refers to ffmpeg's `-copy` flag which copies the input stream instead of
|
||||
re-encoding it, meaning that the cut will process extremely quickly and the
|
||||
resulting video will retain 100% of the original quality. The main drawback is
|
||||
that the cut may have some extra video at the beginning and end, and as a
|
||||
result of that there may be some slightly wonky behavior with video players and
|
||||
editors.
|
||||
|
||||
### Why Would I Re-Encode A Video?
|
||||
- As mentioned above, copying the input stream is very fast and lossless
|
||||
but the cuts are not exact. Sometimes you want a cut to be exact.
|
||||
|
||||
- As mentioned above, copying the input stream is very fast and lossless but
|
||||
the cuts are not exact. Sometimes you want a cut to be exact.
|
||||
|
||||
- If you want to change the framerate.
|
||||
|
||||
- If you want to encode hardsubs.
|
||||
- If the video's compression isn't efficient enough to upload
|
||||
to a messaging platform or something, you may want to compress it more.
|
||||
|
||||
- If the video's compression isn't efficient enough to upload to a messaging
|
||||
platform or something, you may want to compress it more.
|
||||
|
||||
### How Can I Merge (Concatenate) The Resulting Cuts Into One File?
|
||||
|
||||
To concatenate videos with ffmpeg,
|
||||
you need to create a file with content like this:
|
||||
To concatenate videos with ffmpeg, you need to create a file with content like
|
||||
this:
|
||||
|
||||
```
|
||||
file cut_1.mp4
|
||||
file cut_2.mp4
|
||||
file cut_3.mp4
|
||||
file cut_4.mp4
|
||||
```
|
||||
|
||||
You can name the file whatever you want, here I named it `concat.txt`.
|
||||
|
||||
Then run the command:
|
||||
|
||||
```
|
||||
ffmpeg -f concat -safe 0 -i concat.txt -c copy out.mp4
|
||||
```
|
||||
|
||||
That's annoying though, so you can skip
|
||||
manually creating the file by using bash.
|
||||
This command will concatenate all files in the current directory
|
||||
that begin with "COPY_":
|
||||
That's annoying though, so you can skip manually creating the file by using
|
||||
bash. This command will concatenate all files in the current directory that
|
||||
begin with "COPY_":
|
||||
|
||||
```
|
||||
ffmpeg -f concat -safe 0 -i <(printf 'file %q\n' "$PWD"/COPY_*) -c copy lol.mp4
|
||||
```
|
||||
- You need to escape apostrophes which is why
|
||||
we are using `printf %q "$string"`.
|
||||
- Instead of actually creating a file we just
|
||||
use process substitution `<(whatever)` to create a temporary file,
|
||||
which is why we need the `$PWD` in there for the absolute path.
|
||||
|
||||
- You need to escape apostrophes which is why we are using `printf %q
|
||||
"$string"`.
|
||||
|
||||
- Instead of actually creating a file we just use process substitution
|
||||
`<(whatever)` to create a temporary file, which is why we need the `$PWD` in
|
||||
there for the absolute path.
|
||||
|
||||
You can also do it in vim, among other things.
|
||||
|
||||
```
|
||||
ls | vim -
|
||||
:%s/'/\\'/g
|
||||
:%norm Ifile
|
||||
:wq concat.txt
|
||||
```
|
||||
This substitution might not cover all cases, but whatever,
|
||||
if you're concatenating a file named `[{}1;']["!.mp4`
|
||||
you can figure it out yourself.
|
||||
|
||||
This substitution might not cover all cases, but whatever, if you're
|
||||
concatenating a file named `[{}1;']["!.mp4` you can figure it out yourself.
|
||||
|
||||
### Can I Make Seeking And Reverse Playback Faster?
|
||||
|
||||
Depending on the encoding of the video file being played, the following may be quite slow:
|
||||
Depending on the encoding of the video file being played, the following may be
|
||||
quite slow:
|
||||
|
||||
- The use of `exact` in `input.conf`.
|
||||
|
||||
- The use of the `.` and `,` keys to go frame by frame.
|
||||
|
||||
- The holding down of the `,` key to play the video in reverse.
|
||||
|
||||
Long story short, if the video uses an encoding that is difficult for mpv to decode,
|
||||
exact seeking and backwards playback won't be smooth, which for normal playback is not a problem at all,
|
||||
since by default mpv very quickly seeks keyframe-wise when you press `left arrow` or `right arrow`.
|
||||
Long story short, if the video uses an encoding that is difficult for mpv to
|
||||
decode, exact seeking and backwards playback won't be smooth, which for normal
|
||||
playback is not a problem at all, since by default mpv very quickly seeks
|
||||
keyframe-wise when you press `left arrow` or `right arrow`.
|
||||
|
||||
However if we are very intensively cutting a video,
|
||||
it may be useful to be able to quickly seek to an exact time, and to quickly play in reverse.
|
||||
In this case, it is useful to first make a proxy of the original video which is very easy to decode,
|
||||
generate a cut list with the proxy, and then apply the cut list to the original video.
|
||||
However if we are very intensively cutting a video, it may be useful to be able
|
||||
to quickly seek to an exact time, and to quickly play in reverse. In this case,
|
||||
it is useful to first make a proxy of the original video which is very easy to
|
||||
decode, generate a cut list with the proxy, and then apply the cut list to the
|
||||
original video.
|
||||
|
||||
To create a proxy which will be very easy to decode, you can use this ffmpeg command:
|
||||
|
||||
```
|
||||
ffmpeg -noautorotate -i input.mp4 -pix_fmt yuv420p -g 1 -sn -an -vf colormatrix=bt601:bt709,scale=w=1280:h=1280:force_original_aspect_ratio=decrease:force_divisible_by=2 -c:v libx264 -crf 16 -preset superfast -tune fastdecode proxy.mp4
|
||||
```
|
||||
The important options here are the `-g 1` and the scale filter.
|
||||
The other options are more or less irrelevant.
|
||||
The resulting video file should seek extremely quickly
|
||||
and play backwards just fine.
|
||||
|
||||
Once you are done generating the cut list,
|
||||
simply open the `cut_list.txt` file,
|
||||
substitute the proxy file name for the original file name,
|
||||
and run `make_cuts` on it.
|
||||
The important options here are the `-g 1` and the scale filter. The other
|
||||
options are more or less irrelevant. The resulting video file should seek
|
||||
extremely quickly and play backwards just fine.
|
||||
|
||||
## Development
|
||||
|
||||
Uploading releases to GitHub is a pain, so I thought why not create an orphaned branch for releases.
|
||||
|
||||
To set it up, all I did was:
|
||||
```
|
||||
git checkout --orphan release
|
||||
```
|
||||
|
||||
Then, when I want to release a new version, until I find a better way I just do:
|
||||
```
|
||||
TMPDIR=`mktemp -d`
|
||||
cp dist/* "$TMPDIR"
|
||||
git checkout release
|
||||
git rm -r '*'
|
||||
cp "$TMPDIR"/* .
|
||||
git add --all
|
||||
git commit -m "version number"
|
||||
git push
|
||||
```
|
||||
Once you are done generating the cut list, simply open the `cut_list.txt` file,
|
||||
substitute the proxy file name for the original file name, and run `make_cuts`
|
||||
on it.
|
||||
|
|
400
main.lua
400
main.lua
|
@ -1,34 +1,158 @@
|
|||
utils = require "mp.utils"
|
||||
pcall(require, "config")
|
||||
|
||||
mp.msg.info("MPV-CUT LOADED.")
|
||||
|
||||
if USE_GLOBAL_DIR == nil then USE_GLOBAL_DIR = true end
|
||||
if GLOBAL_DIR == nil then GLOBAL_DIR = "~/Desktop" end
|
||||
if USE_CUT_LIST == nil then USE_CUT_LIST = false end
|
||||
if ACTIONS == nil then ACTIONS = { "COPY", "ENCODE" } end
|
||||
if CHANNEL_NAMES == nil then CHANNEL_NAMES = {} end
|
||||
if CHANNEL == nil then CHANNEL = 1 end
|
||||
|
||||
KEY_CUT = KEY_CUT or "c"
|
||||
KEY_MAKE_CUTS = KEY_MAKE_CUTS or "0"
|
||||
KEY_CYCLE_ACTION = KEY_CYCLE_ACTION or "a"
|
||||
KEY_TOGGLE_USE_GLOBAL_DIR = KEY_TOGGLE_USE_GLOBAL_DIR or "g"
|
||||
KEY_TOGGLE_USE_CUT_LIST = KEY_TOGGLE_USE_CUT_LIST or "l"
|
||||
KEY_BOOKMARK_ADD = KEY_BOOKMARK_ADD or "i"
|
||||
KEY_CHANNEL_INC = KEY_CHANNEL_INC or "="
|
||||
KEY_CHANNEL_DEC = KEY_CHANNEL_DEC or "-"
|
||||
|
||||
GLOBAL_DIR = mp.command_native({"expand-path", GLOBAL_DIR})
|
||||
ACTION = ACTIONS[1]
|
||||
MAKE_CUTS_SCRIPT_PATH = utils.join_path(mp.get_script_directory(), "make_cuts")
|
||||
START_TIME = nil
|
||||
|
||||
local function print(s)
|
||||
mp.msg.info(s)
|
||||
mp.osd_message(s)
|
||||
end
|
||||
|
||||
local function table_to_str(o)
|
||||
if type(o) == 'table' then
|
||||
local s = ''
|
||||
for k,v in pairs(o) do
|
||||
if type(k) ~= 'number' then k = '"'..k..'"' end
|
||||
s = s .. '['..k..'] = ' .. table_to_str(v) .. '\n'
|
||||
end
|
||||
return s
|
||||
else
|
||||
return tostring(o)
|
||||
end
|
||||
end
|
||||
|
||||
local function to_hms(seconds)
|
||||
local ms = math.floor((seconds - math.floor(seconds)) * 1000)
|
||||
local secs = math.floor(seconds)
|
||||
local mins = math.floor(secs / 60)
|
||||
secs = secs % 60
|
||||
local hours = math.floor(mins / 60)
|
||||
mins = mins % 60
|
||||
return string.format("%02d-%02d-%02d-%03d", hours, mins, secs, ms)
|
||||
end
|
||||
|
||||
local function next_table_key(t, current)
|
||||
local keys = {}
|
||||
for k in pairs(t) do
|
||||
keys[#keys + 1] = k
|
||||
end
|
||||
table.sort(keys)
|
||||
for i = 1, #keys do
|
||||
if keys[i] == current then
|
||||
return keys[(i % #keys) + 1]
|
||||
end
|
||||
end
|
||||
return keys[1]
|
||||
end
|
||||
|
||||
ACTIONS = {}
|
||||
|
||||
ACTIONS.COPY = function(d)
|
||||
local args = {
|
||||
"ffmpeg",
|
||||
"-nostdin", "-y",
|
||||
"-loglevel", "error",
|
||||
"-ss", d.start_time,
|
||||
"-t", d.duration,
|
||||
"-i", d.inpath,
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-c", "copy",
|
||||
"-map", "0",
|
||||
"-avoid_negative_ts", "make_zero",
|
||||
utils.join_path(d.indir, "COPY_" .. d.channel .. "_" .. d.infile_noext .. "_FROM_" .. d.start_time_hms .. "_TO_" .. d.end_time_hms .. d.ext)
|
||||
}
|
||||
mp.command_native_async({
|
||||
name = "subprocess",
|
||||
args = args,
|
||||
playback_only = false,
|
||||
}, function() print("Done") end)
|
||||
end
|
||||
|
||||
ACTIONS.ENCODE = function(d)
|
||||
local args = {
|
||||
"ffmpeg",
|
||||
"-nostdin", "-y",
|
||||
"-loglevel", "error",
|
||||
"-i", d.inpath,
|
||||
"-ss", d.start_time,
|
||||
"-t", d.duration,
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-crf", "16",
|
||||
"-preset", "superfast",
|
||||
utils.join_path(d.indir, "ENCODE_" .. d.channel .. "_" .. d.infile_noext .. "_FROM_" .. d.start_time_hms .. "_TO_" .. d.end_time_hms .. d.ext)
|
||||
}
|
||||
mp.command_native_async({
|
||||
name = "subprocess",
|
||||
args = args,
|
||||
playback_only = false,
|
||||
}, function() print("Done") end)
|
||||
end
|
||||
|
||||
ACTIONS.LIST = function(d)
|
||||
local inpath = mp.get_property("path")
|
||||
local outpath = inpath .. ".list"
|
||||
local file = io.open(outpath, "a")
|
||||
if not file then print("Error writing to cut list") return end
|
||||
local filesize = file:seek("end")
|
||||
local s = "\n" .. d.channel
|
||||
.. ":" .. d.start_time
|
||||
.. ":" .. d.end_time
|
||||
file:write(s)
|
||||
local delta = file:seek("end") - filesize
|
||||
io.close(file)
|
||||
print("Δ " .. delta)
|
||||
end
|
||||
|
||||
ACTION = "COPY"
|
||||
|
||||
MAKE_CUT = ACTIONS.COPY
|
||||
|
||||
CHANNEL = 1
|
||||
|
||||
CHANNEL_NAMES = {}
|
||||
|
||||
KEY_CUT = "c"
|
||||
KEY_CYCLE_ACTION = "a"
|
||||
KEY_BOOKMARK_ADD = "i"
|
||||
KEY_CHANNEL_INC = "="
|
||||
KEY_CHANNEL_DEC = "-"
|
||||
KEY_MAKE_CUTS = "0"
|
||||
|
||||
pcall(require, "config")
|
||||
|
||||
mp.msg.info("MPV-CUT LOADED.")
|
||||
|
||||
for i, v in ipairs(CHANNEL_NAMES) do
|
||||
CHANNEL_NAMES[i] = string.gsub(v, ":", "-")
|
||||
end
|
||||
|
||||
if not ACTIONS[ACTION] then ACTION = next_table_key(ACTIONS, nil) end
|
||||
|
||||
START_TIME = nil
|
||||
|
||||
local function get_current_channel_name()
|
||||
return CHANNEL_NAMES[CHANNEL] or tostring(CHANNEL)
|
||||
end
|
||||
|
||||
local function get_data()
|
||||
local d = {}
|
||||
d.inpath = mp.get_property("path")
|
||||
d.indir = utils.split_path(d.inpath)
|
||||
d.infile = mp.get_property("filename")
|
||||
d.infile_noext = mp.get_property("filename/no-ext")
|
||||
d.ext = mp.get_property("filename"):match("^.+(%..+)$") or ".mp4"
|
||||
d.channel = get_current_channel_name()
|
||||
return d
|
||||
end
|
||||
|
||||
local function get_times(start_time, end_time)
|
||||
local d = {}
|
||||
d.start_time = tostring(start_time)
|
||||
d.end_time = tostring(end_time)
|
||||
d.duration = tostring(end_time - start_time)
|
||||
d.start_time_hms = tostring(to_hms(start_time))
|
||||
d.end_time_hms = tostring(to_hms(end_time))
|
||||
d.duration_hms = tostring(to_hms(end_time - start_time))
|
||||
return d
|
||||
end
|
||||
|
||||
text_overlay = mp.create_osd_overlay("ass-events")
|
||||
text_overlay.hidden = true
|
||||
text_overlay:update()
|
||||
|
@ -40,18 +164,9 @@ local function text_overlay_off()
|
|||
text_overlay:update()
|
||||
end
|
||||
|
||||
local function get_current_channel_name()
|
||||
return CHANNEL_NAMES[CHANNEL] or CHANNEL
|
||||
end
|
||||
|
||||
local function text_overlay_on()
|
||||
local channel = get_current_channel_name()
|
||||
if USE_CUT_LIST then
|
||||
text_overlay.data = string.format("LIST %s in %s from %s", ACTION, channel, START_TIME)
|
||||
else
|
||||
text_overlay.data = (USE_GLOBAL_DIR and "GLOBAL " or "")
|
||||
.. string.format("%s in %s from %s", ACTION, channel, START_TIME)
|
||||
end
|
||||
text_overlay.data = string.format("%s in %s from %s", ACTION, channel, START_TIME)
|
||||
text_overlay.hidden = false
|
||||
text_overlay:update()
|
||||
end
|
||||
|
@ -60,156 +175,42 @@ local function print_or_update_text_overlay(content)
|
|||
if START_TIME then text_overlay_on() else print(content) end
|
||||
end
|
||||
|
||||
local function toggle_use_cut_list()
|
||||
USE_CUT_LIST = not USE_CUT_LIST
|
||||
print_or_update_text_overlay("USE CUT LIST: " .. tostring(USE_CUT_LIST))
|
||||
end
|
||||
|
||||
local function toggle_use_global_dir()
|
||||
USE_GLOBAL_DIR = not USE_GLOBAL_DIR
|
||||
print_or_update_text_overlay("USE GLOBAL DIR: " .. tostring(USE_GLOBAL_DIR))
|
||||
end
|
||||
|
||||
local function index_of(list, string)
|
||||
local index = 1
|
||||
while index < #list do
|
||||
if list[index] == string then return index end
|
||||
index = index + 1
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function cycle_action()
|
||||
ACTION = ACTIONS[index_of(ACTIONS, ACTION) + 1]
|
||||
ACTION = next_table_key(ACTIONS, ACTION)
|
||||
print_or_update_text_overlay("ACTION: " .. ACTION)
|
||||
end
|
||||
|
||||
local function get_file_info()
|
||||
local inpath = mp.get_property("path")
|
||||
local filename = mp.get_property("filename")
|
||||
local channel_name = get_current_channel_name()
|
||||
return inpath, filename, channel_name
|
||||
end
|
||||
|
||||
local function get_bookmark_file_path()
|
||||
local inpath, filename, channel_name = get_file_info()
|
||||
local indir = utils.split_path(inpath)
|
||||
local outfile = string.format("%s_%s.book", channel_name, filename)
|
||||
return utils.join_path(indir, outfile)
|
||||
end
|
||||
|
||||
local function bookmarks_load()
|
||||
local inpath, filename, channel_name = get_file_info()
|
||||
local inpath = get_bookmark_file_path()
|
||||
local function make_cuts()
|
||||
print("MAKING CUTS")
|
||||
if not MAKE_CUT then print("MAKE_CUT function not found.") return end
|
||||
local inpath = mp.get_property("path") .. ".list"
|
||||
local file = io.open(inpath, "r")
|
||||
if not file then
|
||||
mp.set_property_native("chapter-list", {})
|
||||
return
|
||||
end
|
||||
local arr = {}
|
||||
if not file then print("Error reading cut list") return end
|
||||
for line in file:lines() do
|
||||
if tonumber(line) then
|
||||
table.insert(arr, {
|
||||
time = tonumber(line),
|
||||
title = "chapter_" .. line
|
||||
})
|
||||
if line ~= "" then
|
||||
local cut = {}
|
||||
for token in string.gmatch(line, "[^" .. ":" .. "]+") do
|
||||
table.insert(cut, token)
|
||||
end
|
||||
local d = get_data()
|
||||
d.channel = cut[1]
|
||||
local t = get_times(tonumber(cut[2]), tonumber(cut[3]))
|
||||
for k, v in pairs(t) do d[k] = v end
|
||||
mp.msg.info("MAKE_CUT")
|
||||
mp.msg.info(table_to_str(d))
|
||||
MAKE_CUT(d)
|
||||
end
|
||||
end
|
||||
file:close()
|
||||
table.sort(arr, function(a, b) return a.time < b.time end)
|
||||
mp.set_property_native("chapter-list", arr)
|
||||
end
|
||||
|
||||
local function bookmark_add()
|
||||
local inpath, filename, channel_name = get_file_info()
|
||||
local outpath = get_bookmark_file_path()
|
||||
local file = io.open(outpath, "a")
|
||||
if not file then print("Failed to open bookmark file for writing") return end
|
||||
local out_string = mp.get_property_number("time-pos") .. "\n"
|
||||
local filesize = file:seek("end")
|
||||
file:write(out_string)
|
||||
local delta = file:seek("end") - filesize
|
||||
io.close(file)
|
||||
bookmarks_load()
|
||||
print(string.format("Δ %s, %s", delta, channel_name))
|
||||
end
|
||||
|
||||
local function channel_inc()
|
||||
CHANNEL = CHANNEL + 1
|
||||
bookmarks_load()
|
||||
print_or_update_text_overlay(get_current_channel_name())
|
||||
end
|
||||
|
||||
local function channel_dec()
|
||||
if CHANNEL >= 2 then CHANNEL = CHANNEL - 1 end
|
||||
bookmarks_load()
|
||||
print_or_update_text_overlay(get_current_channel_name())
|
||||
end
|
||||
|
||||
local function print_async_result(success, result, error)
|
||||
print("Done")
|
||||
end
|
||||
|
||||
local function make_cuts()
|
||||
local inpath, filename, channel_name = get_file_info()
|
||||
local indir = utils.split_path(inpath)
|
||||
local file = io.open(inpath .. ".list", "r")
|
||||
local args = { "node", MAKE_CUTS_SCRIPT_PATH, indir }
|
||||
if USE_GLOBAL_DIR then table.insert(args, GLOBAL_DIR) end
|
||||
if file ~= nil then
|
||||
print("Making cuts")
|
||||
local json_string = file:read("*all")
|
||||
table.insert(args, json_string)
|
||||
mp.command_native_async({
|
||||
name = "subprocess",
|
||||
playback_only = false,
|
||||
args = args
|
||||
}, print_async_result)
|
||||
else
|
||||
print("Failed to load cut list")
|
||||
end
|
||||
end
|
||||
|
||||
local function make_cut(json_string)
|
||||
local inpath, filename, channel_name = get_file_info()
|
||||
local indir = utils.split_path(inpath)
|
||||
local args = { "node", MAKE_CUTS_SCRIPT_PATH, indir }
|
||||
if USE_GLOBAL_DIR then table.insert(args, GLOBAL_DIR) end
|
||||
table.insert(args, json_string)
|
||||
print("Making cut")
|
||||
mp.command_native_async({
|
||||
name = "subprocess",
|
||||
playback_only = false,
|
||||
args = args,
|
||||
}, print_async_result)
|
||||
end
|
||||
|
||||
local function write_cut_list(json_string)
|
||||
local inpath, filename, channel_name = get_file_info()
|
||||
local outpath = inpath .. ".list"
|
||||
local file = io.open(outpath, "a")
|
||||
if not file then print("Error writing to cut list") return end
|
||||
local filesize = file:seek("end")
|
||||
file:write(json_string)
|
||||
local delta = file:seek("end") - filesize
|
||||
io.close(file)
|
||||
print("Δ " .. delta)
|
||||
end
|
||||
|
||||
local function cut(start_time, end_time)
|
||||
local inpath, filename, channel_name = get_file_info()
|
||||
local json_string = "{ "
|
||||
.. string.format("%q: %q", "filename", filename)
|
||||
.. string.format(", %q: %q", "action", ACTION)
|
||||
.. string.format(", %q: %q", "channel", channel_name)
|
||||
.. string.format(", %q: %q", "start_time", start_time)
|
||||
.. string.format(", %q: %q", "end_time", end_time)
|
||||
.. " }\n"
|
||||
if USE_CUT_LIST then
|
||||
write_cut_list(json_string)
|
||||
else
|
||||
make_cut(json_string)
|
||||
end
|
||||
local d = get_data()
|
||||
local t = get_times(start_time, end_time)
|
||||
for k, v in pairs(t) do d[k] = v end
|
||||
mp.msg.info(ACTION)
|
||||
mp.msg.info(table_to_str(d))
|
||||
ACTIONS[ACTION](d)
|
||||
end
|
||||
|
||||
local function put_time()
|
||||
|
@ -229,13 +230,62 @@ local function put_time()
|
|||
end
|
||||
end
|
||||
|
||||
mp.add_key_binding(KEY_MAKE_CUTS, "make_cuts", make_cuts)
|
||||
local function get_bookmark_file_path()
|
||||
local d = get_data()
|
||||
mp.msg.info(table_to_str(d))
|
||||
local outfile = string.format("%s_%s.book", d.channel, d.infile)
|
||||
return utils.join_path(d.indir, outfile)
|
||||
end
|
||||
|
||||
local function bookmarks_load()
|
||||
local inpath = get_bookmark_file_path()
|
||||
local file = io.open(inpath, "r")
|
||||
if not file then return end
|
||||
local arr = {}
|
||||
for line in file:lines() do
|
||||
if tonumber(line) then
|
||||
table.insert(arr, {
|
||||
time = tonumber(line),
|
||||
title = "chapter_" .. line
|
||||
})
|
||||
end
|
||||
end
|
||||
file:close()
|
||||
table.sort(arr, function(a, b) return a.time < b.time end)
|
||||
mp.set_property_native("chapter-list", arr)
|
||||
end
|
||||
|
||||
local function bookmark_add()
|
||||
local d = get_data()
|
||||
local outpath = get_bookmark_file_path()
|
||||
local file = io.open(outpath, "a")
|
||||
if not file then print("Failed to open bookmark file for writing") return end
|
||||
local out_string = mp.get_property_number("time-pos") .. "\n"
|
||||
local filesize = file:seek("end")
|
||||
file:write(out_string)
|
||||
local delta = file:seek("end") - filesize
|
||||
io.close(file)
|
||||
bookmarks_load()
|
||||
print(string.format("Δ %s, %s", delta, d.channel))
|
||||
end
|
||||
|
||||
local function channel_inc()
|
||||
CHANNEL = CHANNEL + 1
|
||||
bookmarks_load()
|
||||
print_or_update_text_overlay(get_current_channel_name())
|
||||
end
|
||||
|
||||
local function channel_dec()
|
||||
if CHANNEL >= 2 then CHANNEL = CHANNEL - 1 end
|
||||
bookmarks_load()
|
||||
print_or_update_text_overlay(get_current_channel_name())
|
||||
end
|
||||
|
||||
mp.add_key_binding(KEY_CUT, "cut", put_time)
|
||||
mp.add_key_binding(KEY_BOOKMARK_ADD, "bookmark_add", bookmark_add)
|
||||
mp.add_key_binding(KEY_CHANNEL_INC, "channel_inc", channel_inc)
|
||||
mp.add_key_binding(KEY_CHANNEL_DEC, "channel_dec", channel_dec)
|
||||
mp.add_key_binding(KEY_CYCLE_ACTION, "cycle_action", cycle_action)
|
||||
mp.add_key_binding(KEY_TOGGLE_USE_GLOBAL_DIR, "toggle_use_global_dir", toggle_use_global_dir)
|
||||
mp.add_key_binding(KEY_TOGGLE_USE_CUT_LIST, "toggle_use_cut_list", toggle_use_cut_list)
|
||||
mp.add_key_binding(KEY_MAKE_CUTS, "make_cuts", make_cuts)
|
||||
|
||||
mp.register_event('file-loaded', bookmarks_load)
|
||||
|
|
122
make_cuts.imba
122
make_cuts.imba
|
@ -1,122 +0,0 @@
|
|||
let { readFileSync, statSync } = require 'fs'
|
||||
let { spawnSync } = require 'child_process'
|
||||
let path = require "path"
|
||||
|
||||
let p = console.log
|
||||
let red = "\x1b[31m"
|
||||
let plain = "\x1b[0m"
|
||||
let green = "\x1b[32m"
|
||||
let purple = "\x1b[34m"
|
||||
|
||||
p "IN MAKE_CUTS"
|
||||
|
||||
def quit s
|
||||
p "{red}{s}, quitting.{plain}\n"
|
||||
process.exit!
|
||||
|
||||
def is_dir s
|
||||
try
|
||||
return yes if statSync(s).isDirectory!
|
||||
catch
|
||||
return no
|
||||
|
||||
def parse_json data
|
||||
let failed = new Set!
|
||||
let succeeded = new Set!
|
||||
|
||||
for line in data.split '\n'
|
||||
line = line.trim!
|
||||
continue if line.length < 1
|
||||
try
|
||||
JSON.parse line
|
||||
succeeded.add line
|
||||
catch
|
||||
failed.add line
|
||||
|
||||
failed = [...failed]
|
||||
succeeded = [...succeeded]
|
||||
|
||||
failed.length > 0 and p "\n{red}Failed to load JSON for lines:{plain} {failed}"
|
||||
succeeded.length > 0 and p "\n{green}Cut list:{plain} {succeeded}\n"
|
||||
|
||||
succeeded.map! do |x| JSON.parse(x)
|
||||
|
||||
def to_hms secs
|
||||
[
|
||||
Math.floor secs / 3600
|
||||
Math.floor (secs % 3600) / 60
|
||||
Math.floor ((secs % 3600 % 60) * 1000) / 1000
|
||||
].map(do String($1).padStart(2,0)).join("-") +
|
||||
'.' + String(Math.floor secs % 1 * 1000).padEnd(3,0)
|
||||
|
||||
def main
|
||||
|
||||
let argv = process.argv.slice(2)
|
||||
p "ARGS:"
|
||||
p argv
|
||||
let json = argv.pop!
|
||||
let indir
|
||||
let outdir
|
||||
switch argv.length
|
||||
when 0
|
||||
indir = outdir = "."
|
||||
when 1
|
||||
indir = outdir = argv.pop!
|
||||
when 2
|
||||
[indir, outdir] = argv
|
||||
else
|
||||
quit "Invalid args: {process.argv}"
|
||||
|
||||
let cut_list = parse_json json
|
||||
quit "No valid cuts" if cut_list.length < 1
|
||||
|
||||
quit "Input directory is invalid" if not is_dir indir
|
||||
quit "Output directory is invalid" if not is_dir outdir
|
||||
|
||||
for cut, index in cut_list
|
||||
|
||||
let { filename, action, channel, start_time, end_time } = cut
|
||||
let { name: filename_noext, ext } = path.parse(filename)
|
||||
let duration = parseFloat(end_time) - parseFloat(start_time)
|
||||
|
||||
cut_name = "{action}_{channel}_{filename_noext}_FROM_{to_hms(start_time)}_TO_{to_hms(end_time)}{ext}"
|
||||
|
||||
let inpath = path.join(indir, filename)
|
||||
let outpath = path.join(outdir, cut_name)
|
||||
|
||||
let cmd = "ffmpeg"
|
||||
let args = [
|
||||
"-nostdin", "-y"
|
||||
"-loglevel", "error"
|
||||
"-ss", start_time
|
||||
"-t", duration
|
||||
"-i", inpath
|
||||
"-pix_fmt", "yuv420p"
|
||||
]
|
||||
|
||||
if action is "ENCODE"
|
||||
args.push(
|
||||
"-crf", "16"
|
||||
"-preset", "superfast"
|
||||
)
|
||||
else
|
||||
args.push(
|
||||
"-c", "copy"
|
||||
"-map", "0"
|
||||
"-avoid_negative_ts", "make_zero"
|
||||
)
|
||||
|
||||
args.push(outpath)
|
||||
|
||||
let progress = "({index + 1}/{cut_list.length})"
|
||||
let cmd_str = "{cmd} {args.join(" ")}"
|
||||
|
||||
p "{green}{progress}{plain} {inpath} {green}->{plain}"
|
||||
p "{outpath}\n"
|
||||
p "{purple}{cmd_str}{plain}\n"
|
||||
|
||||
spawnSync cmd, args, { stdio: 'inherit' }
|
||||
|
||||
p "Done.\n"
|
||||
|
||||
main!
|
31
package.json
31
package.json
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"name": "mpv-cut",
|
||||
"version": "1.3.0",
|
||||
"description": "Quickly cut videos both losslessly and re-encoded-ly.",
|
||||
"scripts": {
|
||||
"build:make-dist": "mkdir -p dist",
|
||||
"build:add-hashbang": "echo '#! /usr/bin/env node\n' > dist/make_cuts",
|
||||
"build:compile-imba": "imbac -p make_cuts.imba >> dist/make_cuts",
|
||||
"build:copy-lua": "cp main.lua dist/",
|
||||
"build:copy-package.json": "cp package.json dist/",
|
||||
"build": "npm exec npm-run-all build:*",
|
||||
"release": "npm run build && npx gh-pages --branch release --dist dist/ --message $npm_package_version"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/familyfriendlymikey/mpv-cut.git"
|
||||
},
|
||||
"author": "Mikey Oz",
|
||||
"bugs": {
|
||||
"url": "https://github.com/familyfriendlymikey/mpv-cut/issues"
|
||||
},
|
||||
"homepage": "https://github.com/familyfriendlymikey/mpv-cut#readme",
|
||||
"devDependencies": {},
|
||||
"keywords": [
|
||||
"video",
|
||||
"cut",
|
||||
"mpv",
|
||||
"slice",
|
||||
"editor"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue