mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-23 07:39:52 -06:00
feat: Add a video chapter key for each test. (#8755)
* Add chapters to the video. * Add test. * Use async, await. * Add more asserts. * Install ffprobe if necessary. * Update ffprobePath. Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
This commit is contained in:
@@ -814,7 +814,7 @@ module.exports = {
|
||||
console.log('')
|
||||
},
|
||||
|
||||
async postProcessRecording (name, cname, videoCompression, shouldUploadVideo, quiet) {
|
||||
async postProcessRecording (name, cname, videoCompression, shouldUploadVideo, quiet, ffmpegChaptersConfig) {
|
||||
debug('ending the video recording %o', { name, videoCompression, shouldUploadVideo })
|
||||
|
||||
// once this ended promises resolves
|
||||
@@ -826,7 +826,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
function continueProcessing (onProgress = undefined) {
|
||||
return videoCapture.process(name, cname, videoCompression, onProgress)
|
||||
return videoCapture.process(name, cname, videoCompression, ffmpegChaptersConfig, onProgress)
|
||||
}
|
||||
|
||||
if (quiet) {
|
||||
@@ -1153,12 +1153,15 @@ module.exports = {
|
||||
await openProject.closeBrowser()
|
||||
|
||||
if (endVideoCapture && !videoCaptureFailed) {
|
||||
const ffmpegChaptersConfig = videoCapture.generateFfmpegChaptersConfig(obj.tests)
|
||||
|
||||
await this.postProcessRecording(
|
||||
videoName,
|
||||
compressedVideoName,
|
||||
videoCompression,
|
||||
suv,
|
||||
quiet,
|
||||
ffmpegChaptersConfig,
|
||||
)
|
||||
.catch(warnVideoRecordingFailed)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,33 @@ const deferredPromise = function () {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateFfmpegChaptersConfig (tests) {
|
||||
if (!tests) {
|
||||
return null
|
||||
}
|
||||
|
||||
const configString = tests.map((test) => {
|
||||
return test.attempts.map((attempt, i) => {
|
||||
const { videoTimestamp, wallClockDuration } = attempt
|
||||
let title = test.title ? test.title.join(' ') : ''
|
||||
|
||||
if (i > 0) {
|
||||
title += `attempt ${i}`
|
||||
}
|
||||
|
||||
return [
|
||||
'[CHAPTER]',
|
||||
'TIMEBASE=1/1000',
|
||||
`START=${videoTimestamp - wallClockDuration}`,
|
||||
`END=${videoTimestamp}`,
|
||||
`title=${title}`,
|
||||
].join('\n')
|
||||
}).join('\n')
|
||||
}).join('\n')
|
||||
|
||||
return `;FFMETADATA1\n${configString}`
|
||||
},
|
||||
|
||||
getMsFromDuration (duration) {
|
||||
return utils.timemarkToSeconds(duration) * 1000
|
||||
},
|
||||
@@ -51,6 +78,18 @@ module.exports = {
|
||||
})
|
||||
},
|
||||
|
||||
getChapters (fileName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
ffmpeg.ffprobe(fileName, ['-show_chapters'], (err, metadata) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
resolve(metadata)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
copy (src, dest) {
|
||||
debug('copying from %s to %s', src, dest)
|
||||
|
||||
@@ -217,30 +256,53 @@ module.exports = {
|
||||
})
|
||||
},
|
||||
|
||||
process (name, cname, videoCompression, onProgress = function () {}) {
|
||||
async process (name, cname, videoCompression, ffmpegchaptersConfig, onProgress = function () {}) {
|
||||
const metaFileName = `${name}.meta`
|
||||
|
||||
const maybeGenerateMetaFile = Promise.method(() => {
|
||||
if (!ffmpegchaptersConfig) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Writing the metadata to filesystem is necessary because fluent-ffmpeg is just a wrapper of ffmpeg command.
|
||||
return fs.writeFile(metaFileName, ffmpegchaptersConfig).then(() => true)
|
||||
})
|
||||
|
||||
const addChaptersMeta = await maybeGenerateMetaFile()
|
||||
|
||||
let total = null
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
debug('processing video from %s to %s video compression %o',
|
||||
name, cname, videoCompression)
|
||||
|
||||
ffmpeg()
|
||||
.input(name)
|
||||
.videoCodec('libx264')
|
||||
.outputOptions([
|
||||
const command = ffmpeg()
|
||||
const outputOptions = [
|
||||
'-preset fast',
|
||||
`-crf ${videoCompression}`,
|
||||
])
|
||||
]
|
||||
|
||||
if (addChaptersMeta) {
|
||||
command.input(metaFileName)
|
||||
outputOptions.push('-map_metadata 1')
|
||||
}
|
||||
|
||||
command.input(name)
|
||||
.videoCodec('libx264')
|
||||
.outputOptions(outputOptions)
|
||||
// .videoFilters("crop='floor(in_w/2)*2:floor(in_h/2)*2'")
|
||||
.on('start', (command) => {
|
||||
return debug('compression started %o', { command })
|
||||
}).on('codecData', (data) => {
|
||||
debug('compression started %o', { command })
|
||||
})
|
||||
.on('codecData', (data) => {
|
||||
debug('compression codec data: %o', data)
|
||||
|
||||
total = utils.timemarkToSeconds(data.duration)
|
||||
}).on('stderr', (stderr) => {
|
||||
return debug('compression stderr log %o', { message: stderr })
|
||||
}).on('progress', (progress) => {
|
||||
})
|
||||
.on('stderr', (stderr) => {
|
||||
debug('compression stderr log %o', { message: stderr })
|
||||
})
|
||||
.on('progress', (progress) => {
|
||||
// bail if we dont have total yet
|
||||
if (!total) {
|
||||
return
|
||||
@@ -255,11 +317,13 @@ module.exports = {
|
||||
if (percent < 1) {
|
||||
return onProgress(percent)
|
||||
}
|
||||
}).on('error', (err, stdout, stderr) => {
|
||||
})
|
||||
.on('error', (err, stdout, stderr) => {
|
||||
debug('compression errored: %o', { error: err.message, stdout, stderr })
|
||||
|
||||
return reject(err)
|
||||
}).on('end', () => {
|
||||
})
|
||||
.on('end', () => {
|
||||
debug('compression ended')
|
||||
|
||||
// we are done progressing
|
||||
@@ -269,6 +333,11 @@ module.exports = {
|
||||
return fs.moveAsync(cname, name, {
|
||||
overwrite: true,
|
||||
})
|
||||
.then(() => {
|
||||
if (addChaptersMeta) {
|
||||
return fs.unlink(metaFileName)
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
return resolve()
|
||||
})
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
"@cypress/debugging-proxy": "2.0.1",
|
||||
"@cypress/json-schemas": "5.35.0",
|
||||
"@cypress/sinon-chai": "1.1.0",
|
||||
"@ffprobe-installer/ffprobe": "1.1.0",
|
||||
"@packages/desktop-gui": "*",
|
||||
"@packages/electron": "*",
|
||||
"@packages/example": "*",
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
// ffprobe is necessary to extract chapters data from mp4 files.
|
||||
// ffprobe is usually installed with ffmpeg.
|
||||
// But in our CI, it doesn't. That's why we're installing ffprobe here.
|
||||
const ffprobePath = require('@ffprobe-installer/ffprobe').path
|
||||
const ffmpeg = require('fluent-ffmpeg')
|
||||
|
||||
ffmpeg.setFfprobePath(ffprobePath)
|
||||
|
||||
const humanInterval = require('human-interval')
|
||||
const e2e = require('../support/helpers/e2e').default
|
||||
const glob = require('../../lib/util/glob')
|
||||
@@ -33,7 +41,7 @@ describe('e2e video compression', () => {
|
||||
const videosPath = Fixtures.projectPath('e2e/cypress/videos/*')
|
||||
|
||||
return glob(videosPath)
|
||||
.then((files) => {
|
||||
.tap((files) => {
|
||||
expect(files).to.have.length(1, `globbed for videos and found: ${files.length}. Expected to find 1 video. Search in videosPath: ${videosPath}.`)
|
||||
|
||||
return videoCapture.getCodecData(files[0])
|
||||
@@ -45,6 +53,25 @@ describe('e2e video compression', () => {
|
||||
expect(durationMs).to.be.closeTo(EXPECTED_DURATION_MS, humanInterval('15 seconds'))
|
||||
})
|
||||
})
|
||||
.then((files) => {
|
||||
return videoCapture.getChapters(files[0])
|
||||
.then(({ chapters }) => {
|
||||
// There are 40 chapters but we test only the first one
|
||||
// because what we want to check is if chapters are added properly.
|
||||
// In a chapter object, there are properties like 'end' and 'end_time'.
|
||||
// We don't check them here because they return the test time in milliseconds.
|
||||
// They cannot be guessed correctly and they can cause flakiness.
|
||||
expect(chapters[0].id).to.eq(0)
|
||||
expect(chapters[0].start).to.eq(0)
|
||||
expect(chapters[0].start_time).to.eq(0)
|
||||
expect(chapters[0]['TAG:title']).to.eq('num: 1 makes some long tests')
|
||||
expect(chapters[0].time_base).to.eq('1/1000')
|
||||
expect(chapters[0].end).to.be.a('number')
|
||||
expect(Number.isNaN(chapters[0].end)).to.be.false
|
||||
expect(chapters[0].end_time).to.be.a('number')
|
||||
expect(Number.isNaN(chapters[0].end_time)).to.be.false
|
||||
})
|
||||
})
|
||||
}).get('stdout')
|
||||
.then((stdout) => {
|
||||
expect(stdout).to.match(/Compression progress:\s+\d{1,3}%/)
|
||||
|
||||
48
yarn.lock
48
yarn.lock
@@ -2493,6 +2493,54 @@
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/win32-x64/-/win32-x64-4.1.0.tgz#17e8699b5798d4c60e36e2d6326a8ebe5e95a2c5"
|
||||
integrity sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==
|
||||
|
||||
"@ffprobe-installer/darwin-x64@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ffprobe-installer/darwin-x64/-/darwin-x64-4.1.0.tgz#025c5108faf3e456e6a407dd65b798f8dcc805dd"
|
||||
integrity sha512-ESwvOnbGVGK0r7bUdThSZAYipQOH0X79M4SoNZ5Tg77lq/RVbEdpObNEM2oRfLINbMlQQrezA4VYzt0n/DOkcQ==
|
||||
|
||||
"@ffprobe-installer/ffprobe@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ffprobe-installer/ffprobe/-/ffprobe-1.1.0.tgz#a2f6fbd383f90d9359dc6c0552dca9793a884b3c"
|
||||
integrity sha512-koiZrWEC4hrzCuN+/ijHkeiyx7CnUr/cnGynQAgHMDphsDgZkXivNzZrtT6VI5Nf0SkQqC2ZPU5rx3nZ8yEqLQ==
|
||||
optionalDependencies:
|
||||
"@ffprobe-installer/darwin-x64" "4.1.0"
|
||||
"@ffprobe-installer/linux-arm" "4.3.2"
|
||||
"@ffprobe-installer/linux-arm64" "4.3.2"
|
||||
"@ffprobe-installer/linux-ia32" "4.1.0"
|
||||
"@ffprobe-installer/linux-x64" "4.1.0"
|
||||
"@ffprobe-installer/win32-ia32" "4.1.0"
|
||||
"@ffprobe-installer/win32-x64" "4.1.0"
|
||||
|
||||
"@ffprobe-installer/linux-arm64@4.3.2":
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@ffprobe-installer/linux-arm64/-/linux-arm64-4.3.2.tgz#a64ed27672d55460bdea59bc63da0cf3731f19e8"
|
||||
integrity sha512-9mCINruqx30UqB7kRvc75sj0yAPiDy21Fowow8bQDaAYAuO39MrFt/caLJrX11vCUfx2awolxKeuzTqcO9JjMQ==
|
||||
|
||||
"@ffprobe-installer/linux-arm@4.3.2":
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@ffprobe-installer/linux-arm/-/linux-arm-4.3.2.tgz#09347e67539544168d9815486cd543c3e88cba29"
|
||||
integrity sha512-nZJbpTdh29swlgjVWi2fcV5jvbDFgo2y6a7X/uBsbely/TB158Fg0AncWJm7BbC0CwasGmSdqBsLtoSwXIcrlQ==
|
||||
|
||||
"@ffprobe-installer/linux-ia32@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ffprobe-installer/linux-ia32/-/linux-ia32-4.1.0.tgz#50b5952927b8b1bc42c3f2edd788b995e8470d17"
|
||||
integrity sha512-V2NeZpnly4HP1IU5IrsbbcRg8SWzC/SS0YDNSCjmhxGV2U8MUpW8c8KREE6nX56Dml8B8do5NNkTnaYCDPt3Xw==
|
||||
|
||||
"@ffprobe-installer/linux-x64@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ffprobe-installer/linux-x64/-/linux-x64-4.1.0.tgz#b67c96748457677171ca31b0fb83e4bd3c644ab5"
|
||||
integrity sha512-Id+irHoI+Arq6tb3sHNQyzRrgUVVDgbmwpREDqQ+GDydiCw5ca7VnvRGXE/tBM2mVQ3/6m9wWHR7+xaW3gKJlA==
|
||||
|
||||
"@ffprobe-installer/win32-ia32@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ffprobe-installer/win32-ia32/-/win32-ia32-4.1.0.tgz#19cc6d1043f7e54c2764ada6af8f7f26f98bbad8"
|
||||
integrity sha512-G1pbRfk7XDi9EioT0gSR+O4ARdppS9kSXRzhnJOojUFD6x1k8Qv27RoOXeE5DtIE7TdX6UTywj8qA1BXI5zUUA==
|
||||
|
||||
"@ffprobe-installer/win32-x64@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ffprobe-installer/win32-x64/-/win32-x64-4.1.0.tgz#ed3e8a329eeb6c0625ac439e31ad9ec43001b33c"
|
||||
integrity sha512-gPW2FZxexzCAOhGch0JFkeSSln+wcL5d1JDlJwfSJVEAShHf9MmxiWq0NpHoCSzFvK5qwl0C58KG180eKvd3mA==
|
||||
|
||||
"@fortawesome/fontawesome-free@5.11.2":
|
||||
version "5.11.2"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.11.2.tgz#8644bc25b19475779a7b7c1fc104bc0a794f4465"
|
||||
|
||||
Reference in New Issue
Block a user