Skip to content

Commit 242e99e

Browse files
committed
feat: move screenshots to extra videoScreenshots resolver
1 parent c737173 commit 242e99e

File tree

3 files changed

+124
-96
lines changed

3 files changed

+124
-96
lines changed

packages/gatsby-transformer-video/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
"license": "MIT",
2626
"dependencies": {
2727
"execa": "^4.0.3",
28-
"fast-glob": "^3.2.4",
2928
"fluent-ffmpeg": "^2.1.2",
3029
"fs-extra": "^9.0.1",
3130
"imagemin": "^7.0.1",

packages/gatsby-transformer-video/src/ffmpeg.js

+109-82
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import { resolve, parse } from "path"
1+
import { parse, resolve } from "path"
22
import { performance } from "perf_hooks"
3+
import { tmpdir } from "os"
34

45
import { createContentDigest } from "gatsby-core-utils"
5-
import { pathExists, stat, copy, writeFile } from "fs-extra"
6+
import { pathExists, stat, copy, ensureDir, remove, access } from "fs-extra"
67
import ffmpeg from "fluent-ffmpeg"
7-
import fg from "fast-glob"
88
import imagemin from "imagemin"
99
import imageminGiflossy from "imagemin-giflossy"
1010
import imageminMozjpeg from "imagemin-mozjpeg"
1111
import PQueue from "p-queue"
1212
import sharp from "sharp"
13+
import { createFileNodeFromBuffer } from "gatsby-source-filesystem"
1314

1415
import { cacheContentfulVideo } from "./helpers"
1516

@@ -128,16 +129,14 @@ export default class FFMPEG {
128129
}
129130

130131
// Queue video for conversion
131-
convertVideo = async (...args) => {
132-
const videoData = await this.queue.add(() =>
133-
this.queuedConvertVideo(...args)
134-
)
132+
queueConvertVideo = async (...args) => {
133+
const videoData = await this.queue.add(() => this.convertVideo(...args))
135134

136135
return videoData
137136
}
138137

139138
// Converts a video based on a given profile, populates cache and public dir
140-
queuedConvertVideo = async ({
139+
convertVideo = async ({
141140
profile,
142141
sourcePath,
143142
cachePath,
@@ -174,94 +173,122 @@ export default class FFMPEG {
174173
await copy(cachePath, publicPath, { overwrite: true })
175174
}
176175

177-
// Take screenshots
178-
const screenshots = await this.takeScreenshots({ fieldArgs, publicPath })
179-
180-
return { screenshots, publicPath }
176+
return { publicPath }
181177
}
182178

183-
takeScreenshots = async ({ fieldArgs, publicPath }) => {
184-
const { screenshots, screenshotWidth } = fieldArgs
179+
takeScreenshots = async (
180+
video,
181+
fieldArgs,
182+
{ getCache, createNode, createNodeId }
183+
) => {
184+
const { type } = video.internal
185185

186-
if (!screenshots) {
187-
return null
186+
let fileType = null
187+
if (type === `File`) {
188+
fileType = video.internal.mediaType
188189
}
189190

190-
const { dir: publicDir, name } = parse(publicPath)
191+
if (type === `ContentfulAsset`) {
192+
fileType = video.file.contentType
193+
}
191194

192-
const screenshotPatternCache = resolve(
193-
this.cacheDirConverted,
194-
`${name}-screenshot-*.png`
195-
)
196-
const screenshotPatternPublic = resolve(
197-
publicDir,
198-
`${name}-screenshot-*.jpg`
199-
)
195+
if (fileType.indexOf(`video/`) === -1) {
196+
return null
197+
}
200198

201-
const screenshotsCache = await fg([screenshotPatternCache])
202-
const screenshotsPublic = await fg([screenshotPatternPublic])
199+
let path, fileName
203200

204-
if (!screenshotsCache.length) {
205-
const timestamps = screenshots.split(`,`)
201+
if (type === `File`) {
202+
path = video.absolutePath
203+
fileName = video.name
204+
}
206205

207-
await new Promise((resolve, reject) => {
208-
ffmpeg(publicPath)
209-
.on(`filenames`, function (filenames) {
210-
console.log(`[FFMPEG] Taking ${filenames.length} screenshots`)
211-
})
212-
.on(`error`, (err, stdout, stderr) => {
213-
console.log(`[FFMPEG] Failed to take screenshots:`)
214-
console.error(err)
215-
reject(err)
216-
})
217-
.on(`end`, () => {
218-
resolve()
219-
})
220-
.screenshots({
221-
timestamps,
222-
filename: `${name}-screenshot-%ss.png`,
223-
folder: this.cacheDirConverted,
224-
size: `${screenshotWidth}x?`,
225-
})
206+
if (type === `ContentfulAsset`) {
207+
path = await cacheContentfulVideo({
208+
video,
209+
cacheDir: this.cacheDirOriginal,
226210
})
211+
fileName = video.file.fileName
227212
}
228213

229-
if (!screenshotsPublic.length) {
230-
const screenshotsLatest = await fg([screenshotPatternCache])
231-
for (const rawScreenshotPath of screenshotsLatest) {
232-
const { name: screenshotName } = parse(rawScreenshotPath)
233-
const publicScreenshotPath = resolve(publicDir, `${screenshotName}.jpg`)
214+
const { timestamps, width } = fieldArgs
215+
const { name } = parse(fileName)
216+
217+
const tmpDir = resolve(tmpdir(), `gatsby-transformer-video`, name)
218+
219+
await ensureDir(tmpDir)
220+
221+
let screenshotRawNames
222+
223+
await new Promise((resolve, reject) => {
224+
ffmpeg(path)
225+
.on(`filenames`, function (filenames) {
226+
screenshotRawNames = filenames
227+
console.log(`[FFMPEG] Taking ${filenames.length} screenshots`)
228+
})
229+
.on(`error`, (err, stdout, stderr) => {
230+
console.log(`[FFMPEG] Failed to take screenshots:`)
231+
console.error(err)
232+
reject(err)
233+
})
234+
.on(`end`, () => {
235+
resolve()
236+
})
237+
.screenshots({
238+
timestamps,
239+
filename: `${name}-%ss.png`,
240+
folder: tmpDir,
241+
size: `${width}x?`,
242+
})
243+
})
244+
245+
const screenshotNodes = []
246+
247+
console.log({ screenshotRawNames, tmpDir })
248+
249+
for (const screenshotRawName of screenshotRawNames) {
250+
try {
251+
const rawScreenshotPath = resolve(tmpDir, screenshotRawName)
252+
const { name } = parse(rawScreenshotPath)
234253

235254
try {
236-
const jpgBuffer = await sharp(rawScreenshotPath)
237-
.jpeg({
238-
quality: 60,
239-
progressive: true,
240-
})
241-
.toBuffer()
242-
243-
const optimizedBuffer = await imagemin.buffer(jpgBuffer, {
244-
plugins: [imageminMozjpeg()],
255+
await access(rawScreenshotPath)
256+
} catch {
257+
console.warn(`Screenshot ${rawScreenshotPath} could not be found!`)
258+
continue
259+
}
260+
261+
const jpgBuffer = await sharp(rawScreenshotPath)
262+
.jpeg({
263+
quality: 80,
264+
progressive: true,
245265
})
266+
.toBuffer()
246267

247-
await writeFile(publicScreenshotPath, optimizedBuffer)
248-
} catch (err) {
249-
console.log(`Unable to convert png screenshots to jpegs`)
250-
throw err
251-
}
252-
}
268+
const optimizedBuffer = await imagemin.buffer(jpgBuffer, {
269+
plugins: [imageminMozjpeg()],
270+
})
253271

254-
console.log(`[FFMPEG] Finished copying screenshots`)
272+
const node = await createFileNodeFromBuffer({
273+
ext: `.jpg`,
274+
name,
275+
buffer: optimizedBuffer,
276+
getCache,
277+
createNode,
278+
createNodeId,
279+
})
280+
281+
screenshotNodes.push(node)
282+
} catch (err) {
283+
console.log(`Failed to take screenshots:`)
284+
console.error(err)
285+
throw err
286+
}
255287
}
256288

257-
const latestFiles = await fg([screenshotPatternPublic])
289+
await remove(tmpDir)
258290

259-
return latestFiles.map(absolutePath => {
260-
return {
261-
absolutePath,
262-
path: absolutePath.replace(resolve(this.rootDir, `public`), ``),
263-
}
264-
})
291+
return screenshotNodes
265292
}
266293

267294
createFromProfile = async ({ publicDir, path, name, fieldArgs, info }) => {
@@ -288,7 +315,7 @@ export default class FFMPEG {
288315
const cachePath = resolve(this.cacheDirConverted, filename)
289316
const publicPath = resolve(publicDir, filename)
290317

291-
return this.convertVideo({
318+
return this.queueConvertVideo({
292319
profile: profile.converter,
293320
sourcePath: path,
294321
cachePath,
@@ -303,7 +330,7 @@ export default class FFMPEG {
303330
const cachePath = resolve(this.cacheDirConverted, filename)
304331
const publicPath = resolve(publicDir, filename)
305332

306-
return this.convertVideo({
333+
return this.queueConvertVideo({
307334
profile: profileH264,
308335
sourcePath: path,
309336
cachePath,
@@ -318,7 +345,7 @@ export default class FFMPEG {
318345
const cachePath = resolve(this.cacheDirConverted, filename)
319346
const publicPath = resolve(publicDir, filename)
320347

321-
return this.convertVideo({
348+
return this.queueConvertVideo({
322349
profile: profileH265,
323350
sourcePath: path,
324351
cachePath,
@@ -333,7 +360,7 @@ export default class FFMPEG {
333360
const cachePath = resolve(this.cacheDirConverted, filename)
334361
const publicPath = resolve(publicDir, filename)
335362

336-
return this.convertVideo({
363+
return this.queueConvertVideo({
337364
profile: profileVP9,
338365
sourcePath: path,
339366
cachePath,
@@ -348,7 +375,7 @@ export default class FFMPEG {
348375
const cachePath = resolve(this.cacheDirConverted, filename)
349376
const publicPath = resolve(publicDir, filename)
350377

351-
return this.convertVideo({
378+
return this.queueConvertVideo({
352379
profile: profileWebP,
353380
sourcePath: path,
354381
cachePath,
@@ -363,7 +390,7 @@ export default class FFMPEG {
363390
const cachePath = resolve(this.cacheDirConverted, filename)
364391
const publicPath = resolve(publicDir, filename)
365392

366-
const absolutePath = await this.convertVideo({
393+
const absolutePath = await this.queueConvertVideo({
367394
profile: profileGif,
368395
sourcePath: path,
369396
cachePath,

packages/gatsby-transformer-video/src/gatsby-node.js

+15-13
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ const DEFAULT_ARGS = {
3939
type: GraphQLString,
4040
defaultValue: `assets/videos`,
4141
},
42-
screenshots: { type: GraphQLString },
43-
screenshotWidth: { type: GraphQLInt, defaultValue: 600 },
4442
}
4543

4644
exports.createSchemaCustomization = ({ actions, schema }) => {
@@ -61,14 +59,6 @@ exports.createSchemaCustomization = ({ actions, schema }) => {
6159
duration: GraphQLFloat,
6260
size: GraphQLInt,
6361
bitRate: GraphQLInt,
64-
screenshots: `[GatsbyVideoScreenshot]`,
65-
},
66-
}),
67-
schema.buildObjectType({
68-
name: `GatsbyVideoScreenshot`,
69-
fields: {
70-
path: GraphQLString,
71-
absolutePath: GraphQLString,
7262
},
7363
}),
7464
]
@@ -77,7 +67,7 @@ exports.createSchemaCustomization = ({ actions, schema }) => {
7767
}
7868

7969
exports.createResolvers = async (
80-
{ createResolvers, store },
70+
{ createResolvers, store, getCache, createNodeId, actions: { createNode } },
8171
{ ffmpegPath, ffprobePath, downloadBinaries = true, profiles = {} }
8272
) => {
8373
const program = store.getState().program
@@ -162,7 +152,7 @@ exports.createResolvers = async (
162152
}
163153

164154
// Analyze the resulting video and prepare field return values
165-
async function processResult({ publicPath, screenshots }) {
155+
async function processResult({ publicPath }) {
166156
const result = await ffmpeg.executeFfprobe(publicPath)
167157

168158
const {
@@ -189,7 +179,6 @@ exports.createResolvers = async (
189179
duration: duration === `N/A` ? null : duration,
190180
size: size === `N/A` ? null : size,
191181
bitRate: bitRate === `N/A` ? null : bitRate,
192-
screenshots,
193182
}
194183
}
195184

@@ -290,6 +279,19 @@ exports.createResolvers = async (
290279
transformer: ffmpeg.createFromProfile,
291280
}),
292281
},
282+
videoScreenshots: {
283+
type: `[File]`,
284+
args: {
285+
timestamps: { type: [GraphQLString], defaultValue: [`0`] },
286+
width: { type: GraphQLInt, defaultValue: 600 },
287+
},
288+
resolve: (video, fieldArgs) =>
289+
ffmpeg.takeScreenshots(video, fieldArgs, {
290+
getCache,
291+
createNode,
292+
createNodeId,
293+
}),
294+
},
293295
}
294296

295297
const resolvers = {

0 commit comments

Comments
 (0)