@sns-parse/ext-gif 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/gif.d.ts +13 -0
- package/lib/gif.js +104 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +17 -0
- package/package.json +39 -0
package/lib/gif.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ParserRuntimeLike } from '@sns-parse/core';
|
|
2
|
+
export interface GifOptions {
|
|
3
|
+
maxWidth: number;
|
|
4
|
+
fps: number;
|
|
5
|
+
maxDurationSec: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function resolveFfmpeg(): string;
|
|
8
|
+
/** 生成 ffmpeg 滤镜串(导出供测试) */
|
|
9
|
+
export declare function gifFilter(width: number, fps: number): string;
|
|
10
|
+
/** 裁剪实际转换时长:动图时长与上限取小;未知时长用上限 */
|
|
11
|
+
export declare function gifDuration(durationSec: number, maxDurationSec: number): number;
|
|
12
|
+
/** 下载视频并转 GIF;任何失败返回 null */
|
|
13
|
+
export declare function mp4ToGif(rt: ParserRuntimeLike, url: string, durationSec: number, opts: GifOptions): Promise<Buffer | null>;
|
package/lib/gif.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveFfmpeg = resolveFfmpeg;
|
|
4
|
+
exports.gifFilter = gifFilter;
|
|
5
|
+
exports.gifDuration = gifDuration;
|
|
6
|
+
exports.mp4ToGif = mp4ToGif;
|
|
7
|
+
/**
|
|
8
|
+
* 视频转 GIF(推文动图用)。
|
|
9
|
+
*
|
|
10
|
+
* ffmpeg 管道两遍法(palettegen + paletteuse),不落盘。
|
|
11
|
+
* ffmpeg 不可用 / 转换失败 / 下载失败 → 返回 null,调用方回退发送原视频。
|
|
12
|
+
*/
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
const core_1 = require("@sns-parse/core");
|
|
15
|
+
let ffmpegMissingWarned = false;
|
|
16
|
+
/**
|
|
17
|
+
* ffmpeg 路径解析:内置静态二进制(optionalDependency ffmpeg-static,随插件自动安装)
|
|
18
|
+
* → 系统 PATH。解析结果记忆化,来源记入 debug 日志。
|
|
19
|
+
*/
|
|
20
|
+
let resolvedFfmpeg = null;
|
|
21
|
+
function resolveFfmpeg() {
|
|
22
|
+
if (resolvedFfmpeg !== null)
|
|
23
|
+
return resolvedFfmpeg;
|
|
24
|
+
try {
|
|
25
|
+
const p = require('ffmpeg-static');
|
|
26
|
+
resolvedFfmpeg = typeof p === 'string' && p ? p : 'ffmpeg';
|
|
27
|
+
(0, core_1.debugLog)(`ffmpeg 解析:内置静态二进制 ${resolvedFfmpeg}`);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
resolvedFfmpeg = 'ffmpeg';
|
|
31
|
+
(0, core_1.debugLog)('ffmpeg 解析:未安装 ffmpeg-static(--no-optional 安装?),回退系统 PATH');
|
|
32
|
+
}
|
|
33
|
+
return resolvedFfmpeg;
|
|
34
|
+
}
|
|
35
|
+
/** 生成 ffmpeg 滤镜串(导出供测试) */
|
|
36
|
+
function gifFilter(width, fps) {
|
|
37
|
+
const w = Math.max(120, Math.min(1080, Math.round(width)));
|
|
38
|
+
const f = Math.max(5, Math.min(30, Math.round(fps)));
|
|
39
|
+
return `fps=${f},scale=${w}:-2:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer`;
|
|
40
|
+
}
|
|
41
|
+
/** 裁剪实际转换时长:动图时长与上限取小;未知时长用上限 */
|
|
42
|
+
function gifDuration(durationSec, maxDurationSec) {
|
|
43
|
+
const max = Math.max(1, Math.min(60, maxDurationSec));
|
|
44
|
+
if (!durationSec || durationSec <= 0)
|
|
45
|
+
return max;
|
|
46
|
+
return Math.max(1, Math.min(max, Math.round(durationSec)));
|
|
47
|
+
}
|
|
48
|
+
function runFfmpeg(input, vf, durationSec) {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
const child = (0, child_process_1.spawn)(resolveFfmpeg(), [
|
|
51
|
+
'-hide_banner', '-loglevel', 'error',
|
|
52
|
+
'-i', 'pipe:0',
|
|
53
|
+
'-t', String(durationSec),
|
|
54
|
+
'-vf', vf,
|
|
55
|
+
'-f', 'gif', 'pipe:1',
|
|
56
|
+
]);
|
|
57
|
+
const out = [];
|
|
58
|
+
let err = '';
|
|
59
|
+
child.stdout.on('data', (d) => out.push(d));
|
|
60
|
+
child.stderr.on('data', (d) => { err += d.toString(); });
|
|
61
|
+
child.on('error', (e) => {
|
|
62
|
+
if (!ffmpegMissingWarned) {
|
|
63
|
+
ffmpegMissingWarned = true;
|
|
64
|
+
core_1.logger.warn(`GIF 转换需要 ffmpeg,但二进制不可用(动图将回退为视频发送):${e.message}。请安装 optionalDependency ffmpeg-static,或在 PATH 中提供 ffmpeg。`);
|
|
65
|
+
}
|
|
66
|
+
resolve(null);
|
|
67
|
+
});
|
|
68
|
+
child.on('close', (code) => {
|
|
69
|
+
if (code === 0 && out.length) {
|
|
70
|
+
const gif = Buffer.concat(out);
|
|
71
|
+
core_1.logger.info(`动图已转 GIF 发送:${Math.round(gif.length / 1024)}KB(源视频 ${Math.round(input.length / 1024)}KB)`);
|
|
72
|
+
resolve(gif);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
core_1.logger.info(`动图转 GIF 失败,回退发送原视频(ffmpeg 退出码 ${code}${err ? ':' + err.slice(0, 120) : ''})`);
|
|
76
|
+
resolve(null);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
child.stdin.on('error', () => { });
|
|
80
|
+
child.stdin.end(input);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** 下载视频并转 GIF;任何失败返回 null */
|
|
84
|
+
async function mp4ToGif(rt, url, durationSec, opts) {
|
|
85
|
+
try {
|
|
86
|
+
// X 媒体 CDN (video.twimg.com) 需要浏览器级 UA + Referer 才不会 403(与 vault 下载一致)
|
|
87
|
+
const res = await rt.http.get(url, {
|
|
88
|
+
responseType: 'arraybuffer',
|
|
89
|
+
timeout: Math.min(rt.config.timeout || 60000, 120000),
|
|
90
|
+
headers: {
|
|
91
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
92
|
+
'Referer': 'https://twitter.com/',
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
const input = Buffer.from(res.data);
|
|
96
|
+
if (!input.length)
|
|
97
|
+
return null;
|
|
98
|
+
return await runFfmpeg(input, gifFilter(opts.maxWidth, opts.fps), gifDuration(durationSec, opts.maxDurationSec));
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
core_1.logger.info(`动图 GIF 转换跳过(源视频下载失败,回退发送原视频):${e?.message || e}`);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sns-parse/ext-gif:视频转 GIF(ffmpeg palettegen/paletteuse)。
|
|
3
|
+
*/
|
|
4
|
+
import { mp4ToGif } from './gif';
|
|
5
|
+
export { mp4ToGif, resolveFfmpeg, gifFilter, gifDuration } from './gif';
|
|
6
|
+
export type { GifOptions } from './gif';
|
|
7
|
+
/** 组装为 core 扩展片段 */
|
|
8
|
+
export declare function gifExtension(): {
|
|
9
|
+
mp4ToGif: typeof mp4ToGif;
|
|
10
|
+
};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.gifDuration = exports.gifFilter = exports.resolveFfmpeg = exports.mp4ToGif = void 0;
|
|
4
|
+
exports.gifExtension = gifExtension;
|
|
5
|
+
/**
|
|
6
|
+
* @sns-parse/ext-gif:视频转 GIF(ffmpeg palettegen/paletteuse)。
|
|
7
|
+
*/
|
|
8
|
+
const gif_1 = require("./gif");
|
|
9
|
+
var gif_2 = require("./gif");
|
|
10
|
+
Object.defineProperty(exports, "mp4ToGif", { enumerable: true, get: function () { return gif_2.mp4ToGif; } });
|
|
11
|
+
Object.defineProperty(exports, "resolveFfmpeg", { enumerable: true, get: function () { return gif_2.resolveFfmpeg; } });
|
|
12
|
+
Object.defineProperty(exports, "gifFilter", { enumerable: true, get: function () { return gif_2.gifFilter; } });
|
|
13
|
+
Object.defineProperty(exports, "gifDuration", { enumerable: true, get: function () { return gif_2.gifDuration; } });
|
|
14
|
+
/** 组装为 core 扩展片段 */
|
|
15
|
+
function gifExtension() {
|
|
16
|
+
return { mp4ToGif: gif_1.mp4ToGif };
|
|
17
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sns-parse/ext-gif",
|
|
3
|
+
"version": "0.1.0-alpha.1+upstream.1.6.7",
|
|
4
|
+
"description": "sns-parse 扩展:gif",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"clean": "rimraf lib",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@sns-parse/core": "^0.1.0"
|
|
18
|
+
},
|
|
19
|
+
"optionalDependencies": {
|
|
20
|
+
"ffmpeg-static": "^5.2.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^20.11.0",
|
|
24
|
+
"rimraf": "^5.0.5",
|
|
25
|
+
"typescript": "^5.3.3"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public",
|
|
29
|
+
"registry": "https://registry.npmjs.org"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/sns-parse/extensions.git",
|
|
34
|
+
"directory": "packages/ext-gif"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=16.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|