Skip to content

Commit 3e55e4b

Browse files
committed
Preserve video aspect ratio with ffprobe
1 parent 3e85069 commit 3e55e4b

File tree

5 files changed

+263
-1
lines changed

5 files changed

+263
-1
lines changed

docusaurus.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import remarkNpm2Yarn from '@docusaurus/remark-plugin-npm2yarn';
22
import rehypeCodeblockMeta from './src/plugins/rehype-codeblock-meta.mjs';
33
import rehypeStaticToDynamic from './src/plugins/rehype-static-to-dynamic.mjs';
4+
import rehypeVideoAspectRatio from './src/plugins/rehype-video-aspect-ratio.mjs';
45

56
export default {
67
future: {
@@ -149,6 +150,7 @@ export default {
149150
rehypeCodeblockMeta,
150151
{ match: { snack: true, lang: true, tabs: true } },
151152
],
153+
[rehypeVideoAspectRatio, { staticDir: 'static' }],
152154
rehypeStaticToDynamic,
153155
],
154156
},

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
},
3333
"devDependencies": {
3434
"@babel/types": "^7.28.5",
35+
"@ffprobe-installer/ffprobe": "^2.1.2",
3536
"markdownlint": "^0.36.1",
3637
"markdownlint-cli2": "^0.14.0",
3738
"prettier": "^3.6.2",
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import ffprobe from '@ffprobe-installer/ffprobe';
2+
import { exec } from 'child_process';
3+
import fs from 'fs';
4+
import path from 'path';
5+
import { visit } from 'unist-util-visit';
6+
import { promisify } from 'util';
7+
8+
const execAsync = promisify(exec);
9+
10+
/**
11+
* Rehype plugin to add aspect ratio preservation to video tags
12+
*/
13+
export default function rehypeVideoAspectRatio({ staticDir }) {
14+
return async (tree, file) => {
15+
const promises = [];
16+
17+
visit(tree, 'mdxJsxFlowElement', (node) => {
18+
if (node.name === 'video') {
19+
// Find video source - check src attribute or source children
20+
let videoSrc = null;
21+
22+
// Look for src in attributes
23+
if (node.attributes) {
24+
const srcAttr = node.attributes.find(
25+
(attr) => attr.type === 'mdxJsxAttribute' && attr.name === 'src'
26+
);
27+
28+
if (srcAttr) {
29+
videoSrc = srcAttr.value;
30+
}
31+
}
32+
33+
// If no src attribute, look for source children
34+
if (!videoSrc && node.children) {
35+
const sourceNode = node.children.find(
36+
(child) =>
37+
child.type === 'mdxJsxFlowElement' && child.name === 'source'
38+
);
39+
40+
if (sourceNode?.attributes) {
41+
const srcAttr = sourceNode.attributes.find(
42+
(attr) => attr.type === 'mdxJsxAttribute' && attr.name === 'src'
43+
);
44+
45+
if (srcAttr) {
46+
videoSrc = srcAttr.value;
47+
}
48+
}
49+
}
50+
51+
const isLocalFile =
52+
videoSrc &&
53+
!videoSrc.startsWith('http://') &&
54+
!videoSrc.startsWith('https://') &&
55+
!videoSrc.startsWith('//');
56+
57+
if (isLocalFile) {
58+
const videoPath = path.join(
59+
videoSrc.startsWith('/') ? file.cwd : file.dirname,
60+
staticDir,
61+
videoSrc
62+
);
63+
64+
if (fs.existsSync(videoPath)) {
65+
const promise = getVideoDimensions(videoPath).then((dimensions) => {
66+
if (dimensions.width && dimensions.height) {
67+
applyAspectRatio(node, dimensions.width, dimensions.height);
68+
}
69+
});
70+
71+
promises.push(promise);
72+
} else {
73+
throw new Error(`Video file does not exist (got ${videoPath})`);
74+
}
75+
}
76+
}
77+
});
78+
79+
await Promise.all(promises);
80+
};
81+
}
82+
83+
/**
84+
* Apply aspect ratio styles to a video node
85+
*/
86+
function applyAspectRatio(node, width, height) {
87+
const data = {
88+
estree: {
89+
type: 'Program',
90+
body: [
91+
{
92+
type: 'ExpressionStatement',
93+
expression: {
94+
type: 'ObjectExpression',
95+
properties: [
96+
{
97+
type: 'Property',
98+
key: { type: 'Identifier', name: 'aspectRatio' },
99+
value: { type: 'Literal', value: width / height },
100+
kind: 'init',
101+
},
102+
],
103+
},
104+
},
105+
],
106+
},
107+
};
108+
109+
node.attributes = node.attributes || [];
110+
111+
let styleAttr = node.attributes?.find(
112+
(attr) => attr.type === 'mdxJsxAttribute' && attr.name === 'style'
113+
);
114+
115+
if (styleAttr) {
116+
const properties =
117+
styleAttr.value?.data?.estree?.body?.[0]?.expression?.properties ?? [];
118+
119+
data.estree.body[0].expression.properties.push(...properties);
120+
}
121+
122+
styleAttr = {
123+
type: 'mdxJsxAttribute',
124+
name: 'style',
125+
value: {
126+
type: 'mdxJsxAttributeValueExpression',
127+
data,
128+
},
129+
};
130+
131+
const existingIndex = node.attributes.findIndex(
132+
(attr) => attr.type === 'mdxJsxAttribute' && attr.name === 'style'
133+
);
134+
135+
if (existingIndex !== -1) {
136+
node.attributes[existingIndex] = styleAttr;
137+
} else {
138+
node.attributes.push(styleAttr);
139+
}
140+
}
141+
142+
/**
143+
* Get video dimensions using ffprobe
144+
*/
145+
async function getVideoDimensions(filePath) {
146+
const { stdout } = await execAsync(
147+
`${ffprobe.path} -v error -of flat=s=_ -select_streams v:0 -show_entries stream=height,width "${filePath}"`
148+
);
149+
150+
const lines = stdout.trim().split('\n');
151+
const dimensions = {};
152+
153+
for (const line of lines) {
154+
if (line.includes('width')) {
155+
const width = Number(line.split('=')[1]);
156+
157+
if (Number.isFinite(width) && width > 0) {
158+
dimensions.width = width;
159+
}
160+
} else if (line.includes('height')) {
161+
const height = Number(line.split('=')[1]);
162+
163+
if (Number.isFinite(height) && height > 0) {
164+
dimensions.height = height;
165+
}
166+
}
167+
}
168+
169+
return dimensions;
170+
}

versioned_docs/version-7.x/use-prevent-remove.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,3 @@ Doing so has several benefits:
156156

157157
- This approach still works if the app is closed or crashes unexpectedly.
158158
- It's less intrusive to the user as they can still navigate away from the screen to check something and return without losing the data.
159-
```

yarn.lock

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2202,6 +2202,95 @@ __metadata:
22022202
languageName: node
22032203
linkType: hard
22042204

2205+
"@ffprobe-installer/darwin-arm64@npm:5.0.1":
2206+
version: 5.0.1
2207+
resolution: "@ffprobe-installer/darwin-arm64@npm:5.0.1"
2208+
conditions: os=darwin & cpu=arm64
2209+
languageName: node
2210+
linkType: hard
2211+
2212+
"@ffprobe-installer/darwin-x64@npm:5.1.0":
2213+
version: 5.1.0
2214+
resolution: "@ffprobe-installer/darwin-x64@npm:5.1.0"
2215+
conditions: os=darwin & cpu=x64
2216+
languageName: node
2217+
linkType: hard
2218+
2219+
"@ffprobe-installer/ffprobe@npm:^2.1.2":
2220+
version: 2.1.2
2221+
resolution: "@ffprobe-installer/ffprobe@npm:2.1.2"
2222+
dependencies:
2223+
"@ffprobe-installer/darwin-arm64": "npm:5.0.1"
2224+
"@ffprobe-installer/darwin-x64": "npm:5.1.0"
2225+
"@ffprobe-installer/linux-arm": "npm:5.2.0"
2226+
"@ffprobe-installer/linux-arm64": "npm:5.2.0"
2227+
"@ffprobe-installer/linux-ia32": "npm:5.2.0"
2228+
"@ffprobe-installer/linux-x64": "npm:5.2.0"
2229+
"@ffprobe-installer/win32-ia32": "npm:5.1.0"
2230+
"@ffprobe-installer/win32-x64": "npm:5.1.0"
2231+
dependenciesMeta:
2232+
"@ffprobe-installer/darwin-arm64":
2233+
optional: true
2234+
"@ffprobe-installer/darwin-x64":
2235+
optional: true
2236+
"@ffprobe-installer/linux-arm":
2237+
optional: true
2238+
"@ffprobe-installer/linux-arm64":
2239+
optional: true
2240+
"@ffprobe-installer/linux-ia32":
2241+
optional: true
2242+
"@ffprobe-installer/linux-x64":
2243+
optional: true
2244+
"@ffprobe-installer/win32-ia32":
2245+
optional: true
2246+
"@ffprobe-installer/win32-x64":
2247+
optional: true
2248+
checksum: d3a0e472c9a31bffe10facf6ee0ce4520b4df13d1cf3fdcf7e6ead367f895a348633481c6010c0d9f30a950fe89f791aade9755360047c8dd48d098dccd8414b
2249+
languageName: node
2250+
linkType: hard
2251+
2252+
"@ffprobe-installer/linux-arm64@npm:5.2.0":
2253+
version: 5.2.0
2254+
resolution: "@ffprobe-installer/linux-arm64@npm:5.2.0"
2255+
conditions: os=linux & cpu=arm64
2256+
languageName: node
2257+
linkType: hard
2258+
2259+
"@ffprobe-installer/linux-arm@npm:5.2.0":
2260+
version: 5.2.0
2261+
resolution: "@ffprobe-installer/linux-arm@npm:5.2.0"
2262+
conditions: os=linux & cpu=arm
2263+
languageName: node
2264+
linkType: hard
2265+
2266+
"@ffprobe-installer/linux-ia32@npm:5.2.0":
2267+
version: 5.2.0
2268+
resolution: "@ffprobe-installer/linux-ia32@npm:5.2.0"
2269+
conditions: os=linux & cpu=ia32
2270+
languageName: node
2271+
linkType: hard
2272+
2273+
"@ffprobe-installer/linux-x64@npm:5.2.0":
2274+
version: 5.2.0
2275+
resolution: "@ffprobe-installer/linux-x64@npm:5.2.0"
2276+
conditions: os=linux & cpu=x64
2277+
languageName: node
2278+
linkType: hard
2279+
2280+
"@ffprobe-installer/win32-ia32@npm:5.1.0":
2281+
version: 5.1.0
2282+
resolution: "@ffprobe-installer/win32-ia32@npm:5.1.0"
2283+
conditions: os=win32 & cpu=ia32
2284+
languageName: node
2285+
linkType: hard
2286+
2287+
"@ffprobe-installer/win32-x64@npm:5.1.0":
2288+
version: 5.1.0
2289+
resolution: "@ffprobe-installer/win32-x64@npm:5.1.0"
2290+
conditions: os=win32 & cpu=x64
2291+
languageName: node
2292+
linkType: hard
2293+
22052294
"@hapi/hoek@npm:^9.0.0":
22062295
version: 9.3.0
22072296
resolution: "@hapi/hoek@npm:9.3.0"
@@ -10517,6 +10606,7 @@ __metadata:
1051710606
"@docusaurus/plugin-google-analytics": "npm:3.6.1"
1051810607
"@docusaurus/preset-classic": "npm:3.6.1"
1051910608
"@docusaurus/remark-plugin-npm2yarn": "npm:3.6.1"
10609+
"@ffprobe-installer/ffprobe": "npm:^2.1.2"
1052010610
"@octokit/graphql": "npm:^7.1.0"
1052110611
"@react-navigation/core": "npm:^7.0.4"
1052210612
escape-html: "npm:^1.0.3"

0 commit comments

Comments
 (0)