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:
Kukhyeon Heo
2020-10-11 11:32:12 +09:00
committed by GitHub
parent b56c8c8688
commit 1c6925f952
5 changed files with 164 additions and 16 deletions

View File

@@ -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)
}

View File

@@ -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()
})

View File

@@ -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": "*",

View File

@@ -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}%/)

View File

@@ -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"