mpv是一个很强大的开源跨平台命令行播放器。虽然支持命令行和各种快捷键,但是mpv的gui也很不错,用户界面已经到了可用的水平,ui属于比较简洁的风格。
个人来说使用这款播放器主要是通过快捷键操作,鼠标的用途是滚轮,可以用快进视频,还有右键是暂停视频,还有小窗时移动播放器的位置。
mpv可以配置很多快捷键,也可以配置很多配置项。他最强大的地方是支持用户脚本,这样我们就可以自由地添加自己想要的功能。mpv支持lua和js两种用户脚本。
它的js脚本支持到es5,个人来说更偏好js,因为我们可以用先进的typescript来编码,有很多新的语法糖可以用。而且生态上能用的库比lua多太多了。lua就只有运行快这一个优势了,但是在不用lua-jit的情况下性能差距并没有很明显。
下面介绍的是我编写的mpv-gif.js脚本。
mpv配置文件的最好使用方式是在目录下面创建 portable_config\scripts
,然后脚本放进这个目录里面,作为绿色便携版使用。
这是我存放脚本和配置文件的代码仓库,https://github.com/mudssky/mpv-config,脚本都放在scripts文件夹里。也有说明文档。
本文后面是复制了仓库里的说明文档,并且把脚本的内容复制过来了。
mpv-gif.js
给你的mpv添加制作gif,webp动图的功能,除此之外,还能裁剪音频和视频。
使用g
指定开始时间,G
指定结束时间。
然后就可以用快捷键生成这段时间的gif,视频或是音频。
个人更推荐使用webp动图,如果你上传的平台支持的话,webp动图体积更小画质也更好
运行环境需求
需要ffmepg的命令行工具,并且要放在环境变量里。
feature
- 截取gif或者webp动图,支持带字幕截取,支持外挂字幕。(注意可能有些字幕格式是不支持的。默认会截取第一个字幕或第一个外挂字幕,外挂字幕需要和文件名相同才会引入)
- 无损截取音频
- 无损截取视频
配置说明
你可以在js文件内进行配置。
dir目前是没用的,本来是用来指定存放动图的目录,但是个人没有这个需求,就懒得做了。
frameSize你也可以用1280*720
这样来指定,我这里的默认设置是原视频的三分之一的长和宽,8帧
其他选项下面有注释
// 用户可配置的选项
const userOptions = {
dir: mp.get_property('working-directory'),
//帧大小
frameSize: 'iw/3:ih/3',
//帧率 fps: 15,
fps: 8,
// 设置动图循环播放次数,0是无限循环播放
loop: 0,
// 是否带声音
audio: false,
}
快捷键说明
键位 | 作用 |
---|---|
g | 设置起始时间 |
G | 设置结束时间 |
Ctrl+g | 生成gif动图 |
Ctrl+G | 生成带字幕的gif动图 |
Ctrl+w | 生成webp动图 |
Ctrl+W | 生成带字幕的webp动图 |
Ctrl+a | 截取音频 |
Ctrl+v | 截取视频 |
"use strict";
;
(function (mp) {
var userOptions = {
dir: mp.get_property('working-directory'),
frameSize: 'iw/3:ih/3',
fps: 8,
loop: 0,
audio: false,
};
var envOptions = {
startTime: -1,
endTime: -1,
filename: '',
basename: '',
currentSubFilter: '',
tracksList: [],
audioCodec: '',
};
var animatePicType;
(function (animatePicType) {
animatePicType["webp"] = ".webp";
animatePicType["gif"] = ".gif";
animatePicType["png"] = ".png";
})(animatePicType || (animatePicType = {}));
function validateTime() {
if (envOptions.startTime === -1 ||
envOptions.endTime === -1 ||
envOptions.startTime >= envOptions.endTime) {
mp.osd_message('Invalid start/end time. ');
return false;
}
return true;
}
function initEnvOptions() {
envOptions.filename = mp.get_property('filename') || '';
envOptions.basename = mp.get_property('filename/no-ext') || '';
envOptions.tracksList = JSON.parse(mp.get_property('track-list') || '');
dump('userOption:', userOptions);
dump('envOptions', envOptions);
}
function setStartTime() {
envOptions.startTime = mp.get_property_number('time-pos', -1);
mp.osd_message("GIF Start: " + envOptions.startTime);
}
function setEndTime() {
envOptions.endTime = mp.get_property_number('time-pos', -1);
mp.osd_message("GIF End: " + envOptions.endTime);
}
function pathCorrect(path) {
return path.replace(/\\/g, '/');
}
function getCurrentSub() {
for (var index in envOptions.tracksList) {
var currentObj = envOptions.tracksList[index];
if (currentObj['selected'] && currentObj['type'] === 'sub') {
if (currentObj['external'] === true) {
envOptions.currentSubFilter = "subtitles='" + pathCorrect(currentObj['external-filename']) + "':si=0";
}
else {
envOptions.currentSubFilter = "subtitles='" + envOptions.filename + "'";
}
}
}
}
function getAudioType() {
for (var index in envOptions.tracksList) {
var currentObj = envOptions.tracksList[index];
if (currentObj['selected'] &&
currentObj['type'] === 'audio' &&
currentObj['external'] !== true) {
envOptions.audioCodec = currentObj['codec'];
return currentObj['codec'];
}
}
return '';
}
function geneRateAnimatedPic(picType, hasSubtitles) {
if (!validateTime()) {
return;
}
mp.osd_message('Creating GIF.');
getCurrentSub();
var commands = [];
if (envOptions.currentSubFilter && hasSubtitles) {
commands = [
'ffmpeg',
'-v',
'warning',
'-ss',
"" + envOptions.startTime,
'-i',
"" + envOptions.filename,
'-to',
"" + envOptions.endTime,
'-loop',
"" + userOptions.loop,
'-vf',
"fps=" + userOptions.fps + ",scale=" + userOptions.frameSize + "," + envOptions.currentSubFilter,
envOptions.basename + "[" + envOptions.startTime.toFixed() + "-" + envOptions.endTime.toFixed() + "]" + picType,
];
}
else {
commands = [
'ffmpeg',
'-v',
'warning',
'-i',
"" + envOptions.filename,
'-ss',
"" + envOptions.startTime,
'-to',
"" + envOptions.endTime,
'-loop',
"" + userOptions.loop,
'-vf',
"fps=" + userOptions.fps + ",scale=" + userOptions.frameSize,
envOptions.basename + "[" + envOptions.startTime.toFixed() + "-" + envOptions.endTime.toFixed() + "]" + picType,
];
}
print(commands.join(' '));
mp.command_native_async({
name: 'subprocess',
playback_only: false,
args: commands,
capture_stdout: true,
}, function (success, result, err) {
if (success) {
mp.msg.info("generate " + picType + ":" + envOptions.filename + " succeed");
mp.osd_message("generate " + picType + ":" + envOptions.filename + " succeed");
}
else {
mp.msg.warn("generate " + picType + ":" + envOptions.filename + " failed");
mp.osd_message("generate " + picType + ":" + envOptions.filename + " failed");
}
});
if (userOptions.audio) {
cutAudio();
}
}
function getExt(filename) {
var splitIndex = filename.lastIndexOf('.');
var res = filename.substring(splitIndex + 1);
dump('res', res);
return res;
}
function cutAudio() {
if (!validateTime()) {
return;
}
var AudioType = getAudioType();
var commands;
commands = [
'ffmpeg',
'-v',
'warning',
'-i',
"" + envOptions.filename,
'-accurate_seek',
'-ss',
"" + envOptions.startTime,
'-to',
"" + envOptions.endTime,
'-vn',
'-acodec',
'copy',
envOptions.basename + ".mka",
];
print(commands.join(' '));
mp.command_native_async({
name: 'subprocess',
playback_only: false,
args: commands,
capture_stdout: true,
}, function (success, result, err) {
if (success) {
mp.msg.info("cut audio succeed");
mp.osd_message("Cut Audio Succeed.");
}
else {
mp.msg.warn("cut audio failed");
mp.osd_message("Cut Audio Failed.");
}
});
}
function cutVideo() {
if (!validateTime()) {
return;
}
var commands = [
'ffmpeg',
'-v',
'warning',
'-i',
"" + envOptions.filename,
'-accurate_seek',
'-ss',
"" + envOptions.startTime,
'-to',
"" + envOptions.endTime,
'-c',
'copy',
envOptions.basename + "[" + envOptions.startTime.toFixed() + "-" + envOptions.endTime.toFixed() + "]." + getExt(envOptions.filename),
];
print(commands.join(' '));
mp.command_native_async({
name: 'subprocess',
playback_only: false,
args: commands,
capture_stdout: true,
}, function (success, result, err) {
if (success) {
mp.msg.info("cut video succeed");
mp.osd_message("Cut Video Succeed ");
}
else {
mp.msg.warn("cut video failed");
mp.osd_message("Cut Video failed ");
}
});
}
function generateGif() {
geneRateAnimatedPic(animatePicType.gif, false);
}
function generateGifWithSub() {
geneRateAnimatedPic(animatePicType.gif, true);
}
function generateWebp() {
geneRateAnimatedPic(animatePicType.webp, false);
}
function generateWebpWithSub() {
geneRateAnimatedPic(animatePicType.webp, true);
}
mp.add_key_binding('g', 'setStartTime', setStartTime);
mp.add_key_binding('G', 'setEndTime', setEndTime);
mp.add_key_binding('Ctrl+g', 'generateGif', generateGif);
mp.add_key_binding('Ctrl+G', 'generateGifWithSub', generateGifWithSub);
mp.add_key_binding('Ctrl+w', 'generateWebp', generateWebp);
mp.add_key_binding('Ctrl+W', 'generateWebpWithSub', generateWebpWithSub);
mp.add_key_binding('Ctrl+a', 'cutAudio', cutAudio);
mp.add_key_binding('Ctrl+v', 'cutVideo', cutVideo);
mp.register_event('file-loaded', initEnvOptions);
})(mp);
网友评论