munim-ffmpeg 0.1.1 → 0.3.0
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/NitroMunimFfmpeg.podspec +15 -9
- package/README.md +180 -35
- package/android/build.gradle +2 -2
- package/android/src/main/java/com/margelo/nitro/munimffmpeg/FFmpegNative.kt +86 -0
- package/android/src/main/java/com/margelo/nitro/munimffmpeg/HybridMunimFfmpeg.kt +100 -63
- package/app.plugin.js +58 -2
- package/ios/Bridge.h +4 -1
- package/ios/HybridMunimFfmpeg.swift +176 -90
- package/ios/munim_ffmpeg_core.h +58 -0
- package/lib/index.d.ts +23 -0
- package/lib/index.js +73 -3
- package/package.json +36 -38
- package/scripts/binaries.json +5 -0
- package/scripts/fetch-binaries.mjs +115 -0
- package/src/index.ts +94 -3
- package/scripts/patch-ffmpegkit-level.rb +0 -45
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Platform-neutral core for running FFmpeg 9's own command-line tools inside an
|
|
3
|
+
* app process. The Android JNI bridge and the iOS Swift layer both sit on top
|
|
4
|
+
* of this.
|
|
5
|
+
*/
|
|
6
|
+
#ifndef MUNIM_FFMPEG_CORE_H
|
|
7
|
+
#define MUNIM_FFMPEG_CORE_H
|
|
8
|
+
|
|
9
|
+
#ifdef __cplusplus
|
|
10
|
+
extern "C" {
|
|
11
|
+
#endif
|
|
12
|
+
|
|
13
|
+
typedef void (*munim_log_callback)(void *context, const char *message);
|
|
14
|
+
|
|
15
|
+
typedef void (*munim_statistics_callback)(void *context, double time_ms,
|
|
16
|
+
double size_bytes,
|
|
17
|
+
double bitrate_kbits, double speed,
|
|
18
|
+
double video_frame_number, double fps,
|
|
19
|
+
double quality);
|
|
20
|
+
|
|
21
|
+
/** Version string of the linked FFmpeg, e.g. "9.0.1". */
|
|
22
|
+
const char *munim_ffmpeg_version(void);
|
|
23
|
+
|
|
24
|
+
/** Callbacks apply to whichever execution is currently running. */
|
|
25
|
+
void munim_ffmpeg_set_callbacks(munim_log_callback on_log,
|
|
26
|
+
munim_statistics_callback on_statistics,
|
|
27
|
+
void *context);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Runs `ffmpeg` with the given arguments; `argv[0]` is supplied internally.
|
|
31
|
+
*
|
|
32
|
+
* `stdout_path` receives anything the tool prints rather than logs, such as the
|
|
33
|
+
* `-encoders` and `-protocols` reports. Pass NULL to discard it.
|
|
34
|
+
*
|
|
35
|
+
* fftools keeps its parsed command in file-scope globals, so calls are
|
|
36
|
+
* serialised: a second execution waits for the first to finish.
|
|
37
|
+
*/
|
|
38
|
+
int munim_ffmpeg_execute(int argc, const char *const *argv,
|
|
39
|
+
const char *stdout_path);
|
|
40
|
+
|
|
41
|
+
/** Runs `ffprobe`, writing its report to `output_path` via `-o`. */
|
|
42
|
+
int munim_ffmpeg_probe(int argc, const char *const *argv,
|
|
43
|
+
const char *output_path);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Requests cancellation of the running execution, and of any execution already
|
|
47
|
+
* queued behind it.
|
|
48
|
+
*/
|
|
49
|
+
void munim_ffmpeg_cancel(void);
|
|
50
|
+
|
|
51
|
+
/** Return code reported when a run was cancelled. */
|
|
52
|
+
#define MUNIM_FFMPEG_CANCELLED 255
|
|
53
|
+
|
|
54
|
+
#ifdef __cplusplus
|
|
55
|
+
}
|
|
56
|
+
#endif
|
|
57
|
+
|
|
58
|
+
#endif /* MUNIM_FFMPEG_CORE_H */
|
package/lib/index.d.ts
CHANGED
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
import type { FFmpegLogCallback, FFmpegSessionResult, FFmpegSessionCreatedCallback, FFmpegStatisticsCallback, MunimFfmpeg as MunimFfmpegSpec } from './specs/MunimFfmpeg.nitro';
|
|
2
2
|
declare const MunimFfmpeg: MunimFfmpegSpec;
|
|
3
3
|
export type { FFmpegLogCallback, FFmpegSessionResult, FFmpegSessionCreatedCallback, FFmpegStatisticsCallback, MunimFfmpegSpec, };
|
|
4
|
+
/**
|
|
5
|
+
* Converts a `file://` URI into the plain path FFmpeg expects.
|
|
6
|
+
*
|
|
7
|
+
* `expo-file-system` and `react-native-fs` hand back percent-encoded URIs, but
|
|
8
|
+
* FFmpeg's file protocol treats what follows `file://` literally: a path with a
|
|
9
|
+
* space silently becomes a file named `my%20clip.mp4`. Anything that is not a
|
|
10
|
+
* `file://` URI is returned untouched, so pipes, `content://`, and remote URLs
|
|
11
|
+
* still work.
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizePath(value: string): string;
|
|
4
14
|
export declare function execute(arguments_: string[], onLog?: FFmpegLogCallback, onStatistics?: FFmpegStatisticsCallback, onSessionCreated?: FFmpegSessionCreatedCallback): Promise<FFmpegSessionResult>;
|
|
5
15
|
export declare function probe(arguments_: string[], onLog?: FFmpegLogCallback, onSessionCreated?: FFmpegSessionCreatedCallback): Promise<FFmpegSessionResult>;
|
|
6
16
|
export declare function getMediaInformation(path: string): Promise<unknown>;
|
|
7
17
|
export declare function cancel(sessionId?: number): void;
|
|
8
18
|
export declare function cancelAll(): void;
|
|
9
19
|
export declare function getFFmpegVersion(): string;
|
|
20
|
+
/** Encoder names the bundled FFmpeg build can write, e.g. `libx264`. */
|
|
21
|
+
export declare function listEncoders(): Promise<string[]>;
|
|
22
|
+
/** Decoder names the bundled FFmpeg build can read, e.g. `h264`. */
|
|
23
|
+
export declare function listDecoders(): Promise<string[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Returns the first available encoder from `candidates`, so one command can
|
|
26
|
+
* run on both platforms:
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* const encoder = await pickEncoder(['libx264', 'h264_videotoolbox'])
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function pickEncoder(candidates: string[]): Promise<string | undefined>;
|
|
10
33
|
export default MunimFfmpeg;
|
package/lib/index.js
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import { NitroModules } from 'react-native-nitro-modules';
|
|
2
2
|
const MunimFfmpeg = NitroModules.createHybridObject('MunimFfmpeg');
|
|
3
|
+
const FILE_URI_SCHEME = /^file:\/\//;
|
|
4
|
+
/**
|
|
5
|
+
* Converts a `file://` URI into the plain path FFmpeg expects.
|
|
6
|
+
*
|
|
7
|
+
* `expo-file-system` and `react-native-fs` hand back percent-encoded URIs, but
|
|
8
|
+
* FFmpeg's file protocol treats what follows `file://` literally: a path with a
|
|
9
|
+
* space silently becomes a file named `my%20clip.mp4`. Anything that is not a
|
|
10
|
+
* `file://` URI is returned untouched, so pipes, `content://`, and remote URLs
|
|
11
|
+
* still work.
|
|
12
|
+
*/
|
|
13
|
+
export function normalizePath(value) {
|
|
14
|
+
if (!FILE_URI_SCHEME.test(value))
|
|
15
|
+
return value;
|
|
16
|
+
const path = value.replace(FILE_URI_SCHEME, '');
|
|
17
|
+
try {
|
|
18
|
+
return decodeURIComponent(path);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return path;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
3
24
|
export function execute(arguments_, onLog, onStatistics, onSessionCreated) {
|
|
4
|
-
return MunimFfmpeg.execute(arguments_, onLog, onStatistics, onSessionCreated);
|
|
25
|
+
return MunimFfmpeg.execute(arguments_.map(normalizePath), onLog, onStatistics, onSessionCreated);
|
|
5
26
|
}
|
|
6
27
|
export function probe(arguments_, onLog, onSessionCreated) {
|
|
7
|
-
return MunimFfmpeg.probe(arguments_, onLog, onSessionCreated);
|
|
28
|
+
return MunimFfmpeg.probe(arguments_.map(normalizePath), onLog, onSessionCreated);
|
|
8
29
|
}
|
|
9
30
|
export function getMediaInformation(path) {
|
|
10
|
-
return MunimFfmpeg.getMediaInformation(path).then((value) => JSON.parse(value));
|
|
31
|
+
return MunimFfmpeg.getMediaInformation(normalizePath(path)).then((value) => JSON.parse(value));
|
|
11
32
|
}
|
|
12
33
|
export function cancel(sessionId) {
|
|
13
34
|
MunimFfmpeg.cancel(sessionId);
|
|
@@ -18,4 +39,53 @@ export function cancelAll() {
|
|
|
18
39
|
export function getFFmpegVersion() {
|
|
19
40
|
return MunimFfmpeg.ffmpegVersion;
|
|
20
41
|
}
|
|
42
|
+
// The bundled FFmpeg builds differ per platform: Android ships libx264/libx265,
|
|
43
|
+
// iOS ships the VideoToolbox hardware encoders instead. Asking the binary what
|
|
44
|
+
// it supports is more reliable than hard-coding a per-platform table.
|
|
45
|
+
const codecCache = new Map();
|
|
46
|
+
function listCodecs(flag) {
|
|
47
|
+
const cached = codecCache.get(flag);
|
|
48
|
+
if (cached)
|
|
49
|
+
return cached;
|
|
50
|
+
const request = execute(['-hide_banner', flag])
|
|
51
|
+
.then((result) => {
|
|
52
|
+
if (!result.success) {
|
|
53
|
+
throw new Error(result.failStackTrace ?? result.output);
|
|
54
|
+
}
|
|
55
|
+
// Each entry is printed as `<capability flags> <name> <description>`
|
|
56
|
+
// below a line of dashes.
|
|
57
|
+
const body = result.output.split(/^\s*-+\s*$/m).pop() ?? '';
|
|
58
|
+
return body
|
|
59
|
+
.split('\n')
|
|
60
|
+
.map((line) => line.trim().split(/\s+/))
|
|
61
|
+
.filter((columns) => columns.length >= 2 && /^[A-Z.]{6}$/.test(columns[0]))
|
|
62
|
+
.map((columns) => columns[1]);
|
|
63
|
+
})
|
|
64
|
+
.catch((error) => {
|
|
65
|
+
codecCache.delete(flag);
|
|
66
|
+
throw error;
|
|
67
|
+
});
|
|
68
|
+
codecCache.set(flag, request);
|
|
69
|
+
return request;
|
|
70
|
+
}
|
|
71
|
+
/** Encoder names the bundled FFmpeg build can write, e.g. `libx264`. */
|
|
72
|
+
export function listEncoders() {
|
|
73
|
+
return listCodecs('-encoders');
|
|
74
|
+
}
|
|
75
|
+
/** Decoder names the bundled FFmpeg build can read, e.g. `h264`. */
|
|
76
|
+
export function listDecoders() {
|
|
77
|
+
return listCodecs('-decoders');
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Returns the first available encoder from `candidates`, so one command can
|
|
81
|
+
* run on both platforms:
|
|
82
|
+
*
|
|
83
|
+
* ```ts
|
|
84
|
+
* const encoder = await pickEncoder(['libx264', 'h264_videotoolbox'])
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export async function pickEncoder(candidates) {
|
|
88
|
+
const encoders = new Set(await listEncoders());
|
|
89
|
+
return candidates.find((candidate) => encoders.has(candidate));
|
|
90
|
+
}
|
|
21
91
|
export default MunimFfmpeg;
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "munim-ffmpeg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Fast FFmpeg and FFprobe for Expo and React Native, powered by Nitro Modules",
|
|
5
5
|
"main": "lib/index",
|
|
6
6
|
"module": "lib/index",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"munim-ffmpeg-fetch-binaries": "scripts/fetch-binaries.mjs"
|
|
10
|
+
},
|
|
8
11
|
"react-native": "src/index",
|
|
9
12
|
"source": "src/index",
|
|
10
13
|
"files": [
|
|
@@ -16,30 +19,41 @@
|
|
|
16
19
|
"android/gradle.properties",
|
|
17
20
|
"android/fix-prefab.gradle",
|
|
18
21
|
"android/CMakeLists.txt",
|
|
19
|
-
"android/src",
|
|
22
|
+
"android/src/main/java",
|
|
20
23
|
"cpp",
|
|
21
24
|
"ios/**/*.h",
|
|
22
25
|
"ios/**/*.m",
|
|
23
26
|
"ios/**/*.mm",
|
|
24
27
|
"ios/**/*.cpp",
|
|
25
28
|
"ios/**/*.swift",
|
|
26
|
-
"scripts",
|
|
27
29
|
"app.plugin.js",
|
|
28
30
|
"nitro.json",
|
|
29
31
|
"*.podspec",
|
|
30
32
|
"README.md",
|
|
31
|
-
"LICENSE"
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"android/src/main/cpp",
|
|
35
|
+
"android/src/main/AndroidManifest.xml",
|
|
36
|
+
"scripts/fetch-binaries.mjs",
|
|
37
|
+
"scripts/binaries.json"
|
|
32
38
|
],
|
|
33
39
|
"scripts": {
|
|
34
40
|
"typecheck": "tsc --noEmit",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"lint
|
|
41
|
+
"typecheck:example": "tsc --noEmit -p example/tsconfig.json",
|
|
42
|
+
"clean": "rm -rf lib android/build example/ios example/android",
|
|
43
|
+
"lint": "eslint .",
|
|
44
|
+
"codegen": "nitrogen --logLevel=\"debug\"",
|
|
45
|
+
"specs": "npm run codegen",
|
|
38
46
|
"build": "npm run typecheck && tsc",
|
|
39
47
|
"prepack": "npm run build",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
48
|
+
"check": "npm run codegen && npm run typecheck && npm run typecheck:example && npm run build && npm pack --dry-run",
|
|
49
|
+
"example:start": "npm --workspace example run start",
|
|
50
|
+
"example:ios": "npm --workspace example run ios",
|
|
51
|
+
"example:android": "npm --workspace example run android",
|
|
52
|
+
"release:local": "node scripts/release-local.mjs",
|
|
53
|
+
"format": "prettier --write \"**/*.{ts,tsx,js,cjs,mjs,json,md}\"",
|
|
54
|
+
"postinstall": "node scripts/fetch-binaries.mjs",
|
|
55
|
+
"binaries:build": "scripts/ffmpeg/fetch-source.sh && scripts/ffmpeg/build-all.sh",
|
|
56
|
+
"binaries:package": "scripts/ffmpeg/package.sh"
|
|
43
57
|
},
|
|
44
58
|
"keywords": [
|
|
45
59
|
"react-native",
|
|
@@ -61,6 +75,9 @@
|
|
|
61
75
|
"transcoding",
|
|
62
76
|
"munim-technologies"
|
|
63
77
|
],
|
|
78
|
+
"workspaces": [
|
|
79
|
+
"example"
|
|
80
|
+
],
|
|
64
81
|
"repository": {
|
|
65
82
|
"type": "git",
|
|
66
83
|
"url": "git+https://github.com/munimtechnologies/munim-ffmpeg.git"
|
|
@@ -83,49 +100,30 @@
|
|
|
83
100
|
]
|
|
84
101
|
},
|
|
85
102
|
"devDependencies": {
|
|
86
|
-
"@
|
|
103
|
+
"@eslint/eslintrc": "^3.3.6",
|
|
104
|
+
"@eslint/js": "^9.39.0",
|
|
105
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
106
|
+
"@semantic-release/git": "^10.0.1",
|
|
87
107
|
"@types/react": "^19.2.15",
|
|
108
|
+
"conventional-changelog-conventionalcommits": "^9.3.1",
|
|
88
109
|
"eslint": "^9.39.4",
|
|
89
110
|
"eslint-config-prettier": "^10.1.8",
|
|
90
111
|
"eslint-plugin-prettier": "^5.5.5",
|
|
112
|
+
"globals": "^17.11.0",
|
|
91
113
|
"nitrogen": "0.36.5",
|
|
92
114
|
"prettier": "^3.8.3",
|
|
93
115
|
"react": "19.2.3",
|
|
94
116
|
"react-native": "0.86.2",
|
|
95
117
|
"react-native-nitro-modules": "^0.36.5",
|
|
96
|
-
"
|
|
118
|
+
"semantic-release": "^25.0.9",
|
|
119
|
+
"typescript": "^6.0.3",
|
|
120
|
+
"typescript-eslint": "^8.46.0"
|
|
97
121
|
},
|
|
98
122
|
"peerDependencies": {
|
|
99
123
|
"react": "*",
|
|
100
124
|
"react-native": "*",
|
|
101
125
|
"react-native-nitro-modules": ">=0.36.5 <1"
|
|
102
126
|
},
|
|
103
|
-
"eslintConfig": {
|
|
104
|
-
"root": true,
|
|
105
|
-
"extends": [
|
|
106
|
-
"@react-native",
|
|
107
|
-
"prettier"
|
|
108
|
-
],
|
|
109
|
-
"plugins": [
|
|
110
|
-
"prettier"
|
|
111
|
-
],
|
|
112
|
-
"rules": {
|
|
113
|
-
"prettier/prettier": [
|
|
114
|
-
"warn",
|
|
115
|
-
{
|
|
116
|
-
"quoteProps": "consistent",
|
|
117
|
-
"singleQuote": true,
|
|
118
|
-
"tabWidth": 2,
|
|
119
|
-
"trailingComma": "es5",
|
|
120
|
-
"useTabs": false
|
|
121
|
-
}
|
|
122
|
-
]
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
"eslintIgnore": [
|
|
126
|
-
"node_modules/",
|
|
127
|
-
"lib/"
|
|
128
|
-
],
|
|
129
127
|
"prettier": {
|
|
130
128
|
"quoteProps": "consistent",
|
|
131
129
|
"singleQuote": true,
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Downloads the prebuilt FFmpeg binaries for this version of munim-ffmpeg.
|
|
4
|
+
*
|
|
5
|
+
* They are not published to npm: the bundle is ~200 MB of static libraries and
|
|
6
|
+
* shared objects, which does not belong in a registry tarball. It ships as a
|
|
7
|
+
* GitHub release asset instead, pinned by the checksum in scripts/binaries.json
|
|
8
|
+
* so a build cannot silently pick up different bytes.
|
|
9
|
+
*
|
|
10
|
+
* Runs from postinstall, and can be re-run by hand:
|
|
11
|
+
* npx munim-ffmpeg-fetch-binaries
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from 'node:crypto'
|
|
14
|
+
import { createWriteStream } from 'node:fs'
|
|
15
|
+
import { mkdir, readFile, rm, stat } from 'node:fs/promises'
|
|
16
|
+
import { createRequire } from 'node:module'
|
|
17
|
+
import path from 'node:path'
|
|
18
|
+
import { pipeline } from 'node:stream/promises'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
import { execFile } from 'node:child_process'
|
|
21
|
+
import { promisify } from 'node:util'
|
|
22
|
+
|
|
23
|
+
const run = promisify(execFile)
|
|
24
|
+
const require = createRequire(import.meta.url)
|
|
25
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
26
|
+
|
|
27
|
+
const REPOSITORY = 'munimtechnologies/munim-ffmpeg'
|
|
28
|
+
const MARKERS = [
|
|
29
|
+
'ios/MunimFFmpeg.xcframework',
|
|
30
|
+
'android/src/main/jniLibs/arm64-v8a/libmunimffmpeg9.so',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
async function exists(target) {
|
|
34
|
+
try {
|
|
35
|
+
await stat(target)
|
|
36
|
+
return true
|
|
37
|
+
} catch {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function alreadyInstalled() {
|
|
43
|
+
for (const marker of MARKERS) {
|
|
44
|
+
if (!(await exists(path.join(root, marker)))) return false
|
|
45
|
+
}
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function download(url, destination) {
|
|
50
|
+
const response = await fetch(url, { redirect: 'follow' })
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw new Error(`${response.status} ${response.statusText} for ${url}`)
|
|
53
|
+
}
|
|
54
|
+
await pipeline(response.body, createWriteStream(destination))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function checksum(file) {
|
|
58
|
+
const hash = createHash('sha256')
|
|
59
|
+
hash.update(await readFile(file))
|
|
60
|
+
return hash.digest('hex')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function main() {
|
|
64
|
+
const manifest = require(path.join(root, 'scripts', 'binaries.json'))
|
|
65
|
+
const { version } = require(path.join(root, 'package.json'))
|
|
66
|
+
|
|
67
|
+
if (await alreadyInstalled()) {
|
|
68
|
+
console.log(
|
|
69
|
+
`munim-ffmpeg: FFmpeg ${manifest.ffmpeg} binaries already present`
|
|
70
|
+
)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const url =
|
|
75
|
+
process.env.MUNIM_FFMPEG_BINARIES_URL ??
|
|
76
|
+
`https://github.com/${REPOSITORY}/releases/download/v${version}/${manifest.archive}`
|
|
77
|
+
|
|
78
|
+
const cache = path.join(root, '.binaries-cache')
|
|
79
|
+
await mkdir(cache, { recursive: true })
|
|
80
|
+
const archive = path.join(cache, manifest.archive)
|
|
81
|
+
|
|
82
|
+
console.log(`munim-ffmpeg: downloading FFmpeg ${manifest.ffmpeg} binaries…`)
|
|
83
|
+
await download(url, archive)
|
|
84
|
+
|
|
85
|
+
const actual = await checksum(archive)
|
|
86
|
+
if (actual !== manifest.sha256) {
|
|
87
|
+
await rm(cache, { force: true, recursive: true })
|
|
88
|
+
throw new Error(
|
|
89
|
+
`checksum mismatch for ${manifest.archive}\n expected ${manifest.sha256}\n received ${actual}`
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await run('tar', ['xzf', archive, '-C', root])
|
|
94
|
+
await rm(cache, { force: true, recursive: true })
|
|
95
|
+
|
|
96
|
+
if (!(await alreadyInstalled())) {
|
|
97
|
+
throw new Error('the archive did not contain the expected binaries')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(`munim-ffmpeg: FFmpeg ${manifest.ffmpeg} binaries installed`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
main().catch((error) => {
|
|
104
|
+
console.error(`
|
|
105
|
+
munim-ffmpeg: could not install the native FFmpeg binaries.
|
|
106
|
+
|
|
107
|
+
${error.message}
|
|
108
|
+
|
|
109
|
+
The package cannot build without them. Options:
|
|
110
|
+
• re-run: npx munim-ffmpeg-fetch-binaries
|
|
111
|
+
• use a mirror: MUNIM_FFMPEG_BINARIES_URL=<url> npx munim-ffmpeg-fetch-binaries
|
|
112
|
+
• build them: see scripts/ffmpeg/README.md
|
|
113
|
+
`)
|
|
114
|
+
process.exitCode = 1
|
|
115
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -18,13 +18,40 @@ export type {
|
|
|
18
18
|
MunimFfmpegSpec,
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const FILE_URI_SCHEME = /^file:\/\//
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Converts a `file://` URI into the plain path FFmpeg expects.
|
|
25
|
+
*
|
|
26
|
+
* `expo-file-system` and `react-native-fs` hand back percent-encoded URIs, but
|
|
27
|
+
* FFmpeg's file protocol treats what follows `file://` literally: a path with a
|
|
28
|
+
* space silently becomes a file named `my%20clip.mp4`. Anything that is not a
|
|
29
|
+
* `file://` URI is returned untouched, so pipes, `content://`, and remote URLs
|
|
30
|
+
* still work.
|
|
31
|
+
*/
|
|
32
|
+
export function normalizePath(value: string): string {
|
|
33
|
+
if (!FILE_URI_SCHEME.test(value)) return value
|
|
34
|
+
|
|
35
|
+
const path = value.replace(FILE_URI_SCHEME, '')
|
|
36
|
+
try {
|
|
37
|
+
return decodeURIComponent(path)
|
|
38
|
+
} catch {
|
|
39
|
+
return path
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
21
43
|
export function execute(
|
|
22
44
|
arguments_: string[],
|
|
23
45
|
onLog?: FFmpegLogCallback,
|
|
24
46
|
onStatistics?: FFmpegStatisticsCallback,
|
|
25
47
|
onSessionCreated?: FFmpegSessionCreatedCallback
|
|
26
48
|
): Promise<FFmpegSessionResult> {
|
|
27
|
-
return MunimFfmpeg.execute(
|
|
49
|
+
return MunimFfmpeg.execute(
|
|
50
|
+
arguments_.map(normalizePath),
|
|
51
|
+
onLog,
|
|
52
|
+
onStatistics,
|
|
53
|
+
onSessionCreated
|
|
54
|
+
)
|
|
28
55
|
}
|
|
29
56
|
|
|
30
57
|
export function probe(
|
|
@@ -32,11 +59,15 @@ export function probe(
|
|
|
32
59
|
onLog?: FFmpegLogCallback,
|
|
33
60
|
onSessionCreated?: FFmpegSessionCreatedCallback
|
|
34
61
|
): Promise<FFmpegSessionResult> {
|
|
35
|
-
return MunimFfmpeg.probe(
|
|
62
|
+
return MunimFfmpeg.probe(
|
|
63
|
+
arguments_.map(normalizePath),
|
|
64
|
+
onLog,
|
|
65
|
+
onSessionCreated
|
|
66
|
+
)
|
|
36
67
|
}
|
|
37
68
|
|
|
38
69
|
export function getMediaInformation(path: string): Promise<unknown> {
|
|
39
|
-
return MunimFfmpeg.getMediaInformation(path).then((value) =>
|
|
70
|
+
return MunimFfmpeg.getMediaInformation(normalizePath(path)).then((value) =>
|
|
40
71
|
JSON.parse(value)
|
|
41
72
|
)
|
|
42
73
|
}
|
|
@@ -53,4 +84,64 @@ export function getFFmpegVersion(): string {
|
|
|
53
84
|
return MunimFfmpeg.ffmpegVersion
|
|
54
85
|
}
|
|
55
86
|
|
|
87
|
+
// The bundled FFmpeg builds differ per platform: Android ships libx264/libx265,
|
|
88
|
+
// iOS ships the VideoToolbox hardware encoders instead. Asking the binary what
|
|
89
|
+
// it supports is more reliable than hard-coding a per-platform table.
|
|
90
|
+
const codecCache = new Map<string, Promise<string[]>>()
|
|
91
|
+
|
|
92
|
+
function listCodecs(flag: '-encoders' | '-decoders'): Promise<string[]> {
|
|
93
|
+
const cached = codecCache.get(flag)
|
|
94
|
+
if (cached) return cached
|
|
95
|
+
|
|
96
|
+
const request = execute(['-hide_banner', flag])
|
|
97
|
+
.then((result) => {
|
|
98
|
+
if (!result.success) {
|
|
99
|
+
throw new Error(result.failStackTrace ?? result.output)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Each entry is printed as `<capability flags> <name> <description>`
|
|
103
|
+
// below a line of dashes.
|
|
104
|
+
const body = result.output.split(/^\s*-+\s*$/m).pop() ?? ''
|
|
105
|
+
return body
|
|
106
|
+
.split('\n')
|
|
107
|
+
.map((line) => line.trim().split(/\s+/))
|
|
108
|
+
.filter(
|
|
109
|
+
(columns) => columns.length >= 2 && /^[A-Z.]{6}$/.test(columns[0]!)
|
|
110
|
+
)
|
|
111
|
+
.map((columns) => columns[1]!)
|
|
112
|
+
})
|
|
113
|
+
.catch((error) => {
|
|
114
|
+
codecCache.delete(flag)
|
|
115
|
+
throw error
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
codecCache.set(flag, request)
|
|
119
|
+
return request
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Encoder names the bundled FFmpeg build can write, e.g. `libx264`. */
|
|
123
|
+
export function listEncoders(): Promise<string[]> {
|
|
124
|
+
return listCodecs('-encoders')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Decoder names the bundled FFmpeg build can read, e.g. `h264`. */
|
|
128
|
+
export function listDecoders(): Promise<string[]> {
|
|
129
|
+
return listCodecs('-decoders')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Returns the first available encoder from `candidates`, so one command can
|
|
134
|
+
* run on both platforms:
|
|
135
|
+
*
|
|
136
|
+
* ```ts
|
|
137
|
+
* const encoder = await pickEncoder(['libx264', 'h264_videotoolbox'])
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export async function pickEncoder(
|
|
141
|
+
candidates: string[]
|
|
142
|
+
): Promise<string | undefined> {
|
|
143
|
+
const encoders = new Set(await listEncoders())
|
|
144
|
+
return candidates.find((candidate) => encoders.has(candidate))
|
|
145
|
+
}
|
|
146
|
+
|
|
56
147
|
export default MunimFfmpeg
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'fileutils'
|
|
4
|
-
require 'tempfile'
|
|
5
|
-
|
|
6
|
-
pods_root = ENV.fetch('PODS_ROOT')
|
|
7
|
-
build_products = ENV.fetch('PODS_CONFIGURATION_BUILD_DIR')
|
|
8
|
-
source_root = File.join(pods_root, 'ffmpeg-kit-ios-https-alt', 'ffmpegkit.xcframework')
|
|
9
|
-
|
|
10
|
-
# Patch only the two vendored slices and CocoaPods' selected build product. Nitro's
|
|
11
|
-
# Swift/C++ bridge imports the selected header after this before-compile phase.
|
|
12
|
-
headers = [
|
|
13
|
-
File.join(source_root, 'ios-arm64', 'ffmpegkit.framework', 'Headers', 'Level.h'),
|
|
14
|
-
File.join(source_root, 'ios-arm64_x86_64-simulator', 'ffmpegkit.framework', 'Headers', 'Level.h'),
|
|
15
|
-
File.join(
|
|
16
|
-
build_products,
|
|
17
|
-
'XCFrameworkIntermediates',
|
|
18
|
-
'ffmpeg-kit-ios-https-alt',
|
|
19
|
-
'ffmpegkit.framework',
|
|
20
|
-
'Headers',
|
|
21
|
-
'Level.h'
|
|
22
|
-
),
|
|
23
|
-
].select { |path| File.file?(path) }
|
|
24
|
-
|
|
25
|
-
abort('munim-ffmpeg: could not find the expected FFmpegKit Level.h') if headers.empty?
|
|
26
|
-
|
|
27
|
-
unsigned_declaration = 'NS_ENUM(NSUInteger, Level)'
|
|
28
|
-
signed_declaration = 'NS_ENUM(NSInteger, Level)'
|
|
29
|
-
|
|
30
|
-
headers.each do |header|
|
|
31
|
-
contents = File.binread(header)
|
|
32
|
-
next if contents.include?(signed_declaration) && !contents.include?(unsigned_declaration)
|
|
33
|
-
|
|
34
|
-
occurrences = contents.scan(unsigned_declaration).length
|
|
35
|
-
abort("munim-ffmpeg: unexpected Level enum declaration in #{header}") unless occurrences == 1
|
|
36
|
-
|
|
37
|
-
patched = contents.sub(unsigned_declaration, signed_declaration)
|
|
38
|
-
Tempfile.create(['Level', '.h'], File.dirname(header)) do |temporary|
|
|
39
|
-
temporary.binmode
|
|
40
|
-
temporary.write(patched)
|
|
41
|
-
temporary.flush
|
|
42
|
-
FileUtils.chmod(File.stat(header).mode, temporary.path)
|
|
43
|
-
FileUtils.mv(temporary.path, header)
|
|
44
|
-
end
|
|
45
|
-
end
|