diff --git a/packages/av-cliper/package.json b/packages/av-cliper/package.json index 51178c41..ed6f9ac7 100644 --- a/packages/av-cliper/package.json +++ b/packages/av-cliper/package.json @@ -39,7 +39,7 @@ "typescript": "^4.9.3", "vite": "^5.1.6", "vitest": "^1.4.0", - "webdriverio": "^8.34.0" + "webdriverio": "^8.36.1" }, "dependencies": { "@types/dom-webcodecs": "^0.1.7", diff --git a/packages/av-cliper/src/av-utils.ts b/packages/av-cliper/src/av-utils.ts index 3d41e829..85461e40 100644 --- a/packages/av-cliper/src/av-utils.ts +++ b/packages/av-cliper/src/av-utils.ts @@ -92,7 +92,7 @@ export async function decodeImg( }; const imageDecoder = new ImageDecoder(init); - await Promise.all([imageDecoder.completed, imageDecoder.tracks.ready]) + await Promise.all([imageDecoder.completed, imageDecoder.tracks.ready]); let frameCnt = imageDecoder.tracks.selectedTrack?.frameCount ?? 1; @@ -209,8 +209,8 @@ export function autoReadStream( stream: ST, cbs: { onChunk: ST extends ReadableStream - ? (chunk: DT) => Promise - : never; + ? (chunk: DT) => Promise + : never; onDone: () => void; }, ) { diff --git a/packages/av-cliper/src/clips/__tests__/audio-clip.test.ts b/packages/av-cliper/src/clips/__tests__/audio-clip.test.ts index 55e9b20d..439690f9 100644 --- a/packages/av-cliper/src/clips/__tests__/audio-clip.test.ts +++ b/packages/av-cliper/src/clips/__tests__/audio-clip.test.ts @@ -93,3 +93,14 @@ test('seek', async () => { const { audio: audio3 } = await clip.tick(11e6); expect(audio3[0].length).toBe(DEFAULT_AUDIO_CONF.sampleRate * 2); }); + +test('split by time', async () => { + const clip = new AudioClip((await fetch(m4a_44kHz_2chan)).body!); + const [preClip1, postClip2] = await clip.split(60e6); + expect(Math.round(preClip1.meta.duration / 1e6)).toBe(60); + expect(Math.round(postClip2.meta.duration / 1e6)).toBe(62); + + const [preClip11, postClip12] = await preClip1.split(30e6); + expect(Math.round(preClip11.meta.duration / 1e6)).toBe(30); + expect(Math.round(postClip12.meta.duration / 1e6)).toBe(30); +}); diff --git a/packages/av-cliper/src/clips/__tests__/embed-subtitles.test.ts b/packages/av-cliper/src/clips/__tests__/embed-subtitles.test.ts index a47fec07..bf43d93f 100644 --- a/packages/av-cliper/src/clips/__tests__/embed-subtitles.test.ts +++ b/packages/av-cliper/src/clips/__tests__/embed-subtitles.test.ts @@ -1,5 +1,4 @@ import { expect, test } from 'vitest'; -// import '../../__tests__/mock'; import { EmbedSubtitlesClip } from '../embed-subtitles-clip'; const txt1 = ` @@ -80,3 +79,15 @@ test('EmbedSubtitles digital', async () => { expect(vf1?.timestamp).toBe(342000); expect(vf1?.duration).toBe(3218000 - 342000); }); + +test('split by time', async () => { + const clip = new EmbedSubtitlesClip(txt1, { + videoWidth: 1280, + videoHeight: 720, + }); + await clip.ready; + const [preClip, postClip] = await clip.split(30e6); + expect(clip.meta.duration).toBe( + preClip.meta.duration + postClip.meta.duration, + ); +}); diff --git a/packages/av-cliper/src/clips/__tests__/img-clip.test.ts b/packages/av-cliper/src/clips/__tests__/img-clip.test.ts new file mode 100644 index 00000000..ec64c790 --- /dev/null +++ b/packages/av-cliper/src/clips/__tests__/img-clip.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from 'vitest'; +import { ImgClip } from '..'; + +const animated_gif = `//${location.host}/img/animated.gif`; + +test('split by time', async () => { + const clip = new ImgClip({ + type: 'image/gif', + stream: (await fetch(animated_gif)).body!, + }); + await clip.ready; + const [preClip, postClip] = await clip.split(1e6); + expect(clip.meta.duration).toBe( + preClip.meta.duration + postClip.meta.duration, + ); +}); diff --git a/packages/av-cliper/src/clips/__tests__/mp4-clip.test.ts b/packages/av-cliper/src/clips/__tests__/mp4-clip.test.ts index dd5a84d9..a2c322d8 100644 --- a/packages/av-cliper/src/clips/__tests__/mp4-clip.test.ts +++ b/packages/av-cliper/src/clips/__tests__/mp4-clip.test.ts @@ -115,7 +115,7 @@ test('split track', async () => { expect(Math.round(trackClips[1].meta.duration / 1e6)).toBe(21); }); -test('split when only has video track', async () => { +test('splitTrack when only has video track', async () => { const clip = new MP4Clip((await fetch(mp4_bunny_1)).body!, { audio: false }); await clip.ready; const trackClips = await clip.splitTrack(); @@ -125,3 +125,16 @@ test('split when only has video track', async () => { expect(trackClips[0].meta.audioChanCount).toBe(0); expect(Math.round(trackClips[0].meta.duration / 1e6)).toBe(21); }); + +test('split MP4Clip by time', async () => { + const clip = new MP4Clip((await fetch(mp4_bunny_1)).body!); + const [preClip1, postClip2] = await clip.split(10e6); + expect(Math.round(preClip1.meta.duration / 1e6)).toEqual(10); + expect(Math.round(postClip2.meta.duration / 1e6)).toEqual(11); + expect(preClip1.meta.audioChanCount).toBe(2); + expect(postClip2.meta.audioChanCount).toBe(2); + // second split + const [preClip11, postClip12] = await preClip1.split(5e6); + expect(Math.round(preClip11.meta.duration / 1e6)).toEqual(5); + expect(Math.round(postClip12.meta.duration / 1e6)).toEqual(5); +}); diff --git a/packages/av-cliper/src/clips/audio-clip.ts b/packages/av-cliper/src/clips/audio-clip.ts index eb5a7590..8773f5f4 100644 --- a/packages/av-cliper/src/clips/audio-clip.ts +++ b/packages/av-cliper/src/clips/audio-clip.ts @@ -23,6 +23,14 @@ export class AudioClip implements IClip { height: 0, }; + get meta() { + return { + duration: this.#meta.duration, + sampleRate: DEFAULT_AUDIO_CONF.sampleRate, + chanCount: 2, + }; + } + #chan0Buf = new Float32Array(); #chan1Buf = new Float32Array(); getPCMData(): Float32Array[] { @@ -133,6 +141,20 @@ export class AudioClip implements IClip { return { audio, state: 'success' }; } + async split(time: number) { + await this.ready; + const frameCnt = Math.ceil((time / 1e6) * DEFAULT_AUDIO_CONF.sampleRate); + const preSlice = new AudioClip( + this.getPCMData().map((chan) => chan.slice(0, frameCnt)), + this.#opts, + ); + const postSlice = new AudioClip( + this.getPCMData().map((chan) => chan.slice(frameCnt)), + this.#opts, + ); + return [preSlice, postSlice]; + } + async clone() { return new AudioClip(this.getPCMData(), this.#opts) as this; } diff --git a/packages/av-cliper/src/clips/embed-subtitles-clip.ts b/packages/av-cliper/src/clips/embed-subtitles-clip.ts index c6c1b24c..d7f68f0e 100644 --- a/packages/av-cliper/src/clips/embed-subtitles-clip.ts +++ b/packages/av-cliper/src/clips/embed-subtitles-clip.ts @@ -29,14 +29,26 @@ declare global { } } +interface SubtitleStruct { + start: number; + end: number; + text: string; +} + export class EmbedSubtitlesClip implements IClip { ready: IClip['ready']; - #subtitles: Array<{ - start: number; - end: number; - text: string; - }> = []; + #subtitles: SubtitleStruct[] = []; + + #meta = { + width: 0, + height: 0, + duration: 0, + }; + + get meta() { + return { ...this.#meta }; + } #opts: Required = { color: '#FFF', @@ -68,15 +80,14 @@ export class EmbedSubtitlesClip implements IClip { #lineHeight = 0; #linePadding = 0; - #content; - - constructor(content: string, opts: IEmbedSubtitlesOpts) { - this.#content = content; - this.#subtitles = parseSrt(content).map(({ start, end, text }) => ({ - start: start * 1e6, - end: end * 1e6, - text, - })); + constructor(content: string | SubtitleStruct[], opts: IEmbedSubtitlesOpts) { + this.#subtitles = Array.isArray(content) + ? content + : parseSrt(content).map(({ start, end, text }) => ({ + start: start * 1e6, + end: end * 1e6, + text, + })); if (this.#subtitles.length === 0) throw Error('No subtitles content'); this.#opts = Object.assign(this.#opts, opts); @@ -94,12 +105,13 @@ export class EmbedSubtitlesClip implements IClip { this.#ctx.textBaseline = 'top'; this.#ctx.letterSpacing = letterSpacing ?? '0px'; - // 字幕的宽高 由视频画面内容决定 - this.ready = Promise.resolve({ + this.#meta = { width: videoWidth, height: videoHeight, duration: this.#subtitles.at(-1)?.end ?? 0, - }); + }; + // 字幕的宽高 由视频画面内容决定 + this.ready = Promise.resolve(this.meta); } #renderTxt(txt: string) { @@ -225,8 +237,41 @@ export class EmbedSubtitlesClip implements IClip { return { video: vf.clone(), state: 'success' }; } + async split(time: number) { + await this.ready; + let hitIdx = -1; + for (let i = 0; i < this.#subtitles.length; i++) { + const sub = this.#subtitles[i]; + if (time > sub.start) continue; + hitIdx = i; + break; + } + if (hitIdx === -1) throw Error('Not found subtitle by time'); + const preSlice = this.#subtitles.slice(0, hitIdx).map((s) => ({ ...s })); + let preLastIt = preSlice.at(-1); + let postFirstIt = null; + // 切割时间命中字幕区间,需要将当前字幕元素拆成前后两份 + if (preLastIt != null && preLastIt.end > time) { + postFirstIt = { + start: 0, + end: preLastIt.end - time, + text: preLastIt.text, + }; + + preLastIt.end = time; + } + const postSlice = this.#subtitles + .slice(hitIdx) + .map((s) => ({ ...s, start: s.start - time, end: s.end - time })); + if (postFirstIt != null) postSlice.unshift(postFirstIt); + return [ + new EmbedSubtitlesClip(preSlice, this.#opts), + new EmbedSubtitlesClip(postSlice, this.#opts), + ] as this[]; + } + async clone() { - return new EmbedSubtitlesClip(this.#content, this.#opts) as this; + return new EmbedSubtitlesClip(this.#subtitles.slice(0), this.#opts) as this; } destroy() { diff --git a/packages/av-cliper/src/clips/img-clip.ts b/packages/av-cliper/src/clips/img-clip.ts index ef15f441..972811c3 100644 --- a/packages/av-cliper/src/clips/img-clip.ts +++ b/packages/av-cliper/src/clips/img-clip.ts @@ -2,6 +2,8 @@ import { decodeImg } from '../av-utils'; import { Log } from '../log'; import { IClip } from './iclip'; +type AnimateImgType = 'avif' | 'webp' | 'png' | 'gif'; + export class ImgClip implements IClip { ready: IClip['ready']; @@ -12,6 +14,10 @@ export class ImgClip implements IClip { height: 0, }; + get meta() { + return { ...this.#meta }; + } + #img: ImageBitmap | null = null; #frames: VideoFrame[] = []; @@ -20,7 +26,7 @@ export class ImgClip implements IClip { dataSource: | ImageBitmap | VideoFrame[] - | { type: 'image/gif'; stream: ReadableStream }, + | { type: `image/${AnimateImgType}`; stream: ReadableStream }, ) { if (dataSource instanceof ImageBitmap) { this.#img = dataSource; @@ -45,17 +51,21 @@ export class ImgClip implements IClip { }; this.ready = Promise.resolve({ ...this.#meta, duration: Infinity }); } else { - this.ready = this.#gifInit(dataSource.stream, dataSource.type).then( - () => ({ - width: this.#meta.width, - height: this.#meta.height, - duration: Infinity, - }), - ); + this.ready = this.#initAnimateImg( + dataSource.stream, + dataSource.type, + ).then(() => ({ + width: this.#meta.width, + height: this.#meta.height, + duration: Infinity, + })); } } - async #gifInit(stream: ReadableStream, type: string) { + async #initAnimateImg( + stream: ReadableStream, + type: `image/${AnimateImgType}`, + ) { this.#frames = await decodeImg(stream, type); const firstVf = this.#frames[0]; if (firstVf == null) throw Error('No frame available in gif'); @@ -90,6 +100,31 @@ export class ImgClip implements IClip { }; } + async split(time: number) { + await this.ready; + if (this.#img != null) { + return [new ImgClip(this.#img), new ImgClip(this.#img)]; + } + let hitIdx = -1; + for (let i = 0; i < this.#frames.length; i++) { + const vf = this.#frames[i]; + if (time > vf.timestamp) continue; + hitIdx = i; + break; + } + if (hitIdx === -1) throw Error('Not found frame by time'); + const preSlice = this.#frames + .slice(0, hitIdx) + .map((vf) => new VideoFrame(vf)); + const postSlice = this.#frames.slice(hitIdx).map( + (vf) => + new VideoFrame(vf, { + timestamp: vf.timestamp - time, + }), + ); + return [new ImgClip(preSlice), new ImgClip(postSlice)] as this[]; + } + async clone() { await this.ready; return new ImgClip(this.#img ?? this.#frames) as this; diff --git a/packages/av-cliper/src/clips/mp4-clip.ts b/packages/av-cliper/src/clips/mp4-clip.ts index da15b19b..0df1a718 100644 --- a/packages/av-cliper/src/clips/mp4-clip.ts +++ b/packages/av-cliper/src/clips/mp4-clip.ts @@ -25,6 +25,14 @@ interface MP4ClipOpts { audio?: boolean | { volume: number }; } +type ExtMP4Sample = MP4Sample & { deleted?: boolean }; + +type ThumbnailOpts = { + start: number; + end: number; + step: number; +}; + export class MP4Clip implements IClip { #log = Log.create(`MP4Clip id:${CLIP_ID++},`); @@ -47,9 +55,9 @@ export class MP4Clip implements IClip { #volume = 1; - #videoSamples: Array = []; + #videoSamples: ExtMP4Sample[] = []; - #audioSamples: Array = []; + #audioSamples: ExtMP4Sample[] = []; #videoFrameFinder: VideoFrameFinder | null = null; #audioFrameFinder: AudioFrameFinder | null = null; @@ -100,8 +108,8 @@ export class MP4Clip implements IClip { function genDeocder( decoderConf: MP4DecoderConf, - videoSamples: MP4Sample[], - audioSamples: MP4Sample[], + videoSamples: ExtMP4Sample[], + audioSamples: ExtMP4Sample[], volume: number | null, ) { return { @@ -123,8 +131,8 @@ export class MP4Clip implements IClip { function genMeta( decoderConf: MP4DecoderConf, - videoSamples: MP4Sample[], - audioSamples: MP4Sample[], + videoSamples: ExtMP4Sample[], + audioSamples: ExtMP4Sample[], ) { const meta = { duration: 0, @@ -142,8 +150,15 @@ export class MP4Clip implements IClip { meta.audioChanCount = DEFAULT_AUDIO_CONF.channelCount; } - const lastSampele = videoSamples.at(-1) ?? audioSamples.at(-1); - if (lastSampele != null) { + if (videoSamples.length > 0) { + for (let i = videoSamples.length - 1; i >= 0; i--) { + const s = videoSamples[i]; + if (s.deleted) continue; + meta.duration = s.cts + s.duration; + break; + } + } else if (audioSamples.length > 0) { + const lastSampele = audioSamples.at(-1)!; meta.duration = lastSampele.cts + lastSampele.duration; } @@ -334,6 +349,40 @@ export class MP4Clip implements IClip { }); } + async split(time: number) { + await this.ready; + + if (time <= 0 || time >= this.#meta.duration) + throw Error('"time" out of bounds'); + + const [preVideoSlice, postVideoSlice] = splitVideoSampleByTime( + this.#videoSamples, + time, + ); + const [preAudioSlice, postAudioSlice] = splitAudioSampleByTime( + this.#audioSamples, + time, + ); + const preClip = new MP4Clip( + { + videoSamples: preVideoSlice, + audioSamples: preAudioSlice, + decoderConf: this.#decoderConf, + }, + this.#opts, + ); + const postClip = new MP4Clip( + { + videoSamples: postVideoSlice, + audioSamples: postAudioSlice, + decoderConf: this.#decoderConf, + }, + this.#opts, + ); + + return [preClip, postClip]; + } + async clone() { await this.ready; const clip = new MP4Clip( @@ -827,8 +876,63 @@ function createVF2BlobConvtr( }; } -export type ThumbnailOpts = { - start: number; - end: number; - step: number; -}; +function splitVideoSampleByTime(videoSamples: ExtMP4Sample[], time: number) { + let gopStartIdx = 0; + let gopEndIdx = 0; + let hitIdx = -1; + for (let i = 0; i < videoSamples.length; i++) { + const s = videoSamples[i]; + if (hitIdx === -1 && time < s.cts) hitIdx = i - 1; + if (s.is_sync) { + if (hitIdx === -1) { + gopStartIdx = i; + } else { + gopEndIdx = i; + break; + } + } + } + + const hitSample = videoSamples[hitIdx]; + if (hitSample == null) throw Error('Not found video sample by time'); + + const preSlice = videoSamples + .slice(0, gopEndIdx === 0 ? videoSamples.length : gopEndIdx) + .map((s) => ({ ...s })); + for (let i = gopStartIdx; i < preSlice.length; i++) { + const s = preSlice[i]; + if (time < s.cts) { + s.deleted = true; + s.cts = -1; + } + } + + const postSlice = videoSamples + .slice(hitSample.is_sync ? gopEndIdx : gopStartIdx) + .map((s) => ({ ...s, cts: s.cts - time })); + for (let i = 0; i < gopEndIdx - gopEndIdx; i++) { + const s = preSlice[i]; + if (s.cts < 0) { + s.deleted = true; + s.cts = -1; + } + } + + return [preSlice, postSlice]; +} + +function splitAudioSampleByTime(audioSamples: ExtMP4Sample[], time: number) { + let hitIdx = -1; + for (let i = 0; i < audioSamples.length; i++) { + const s = audioSamples[i]; + if (time > s.cts) continue; + hitIdx = i; + break; + } + if (hitIdx === -1) throw Error('Not found audio sample by time'); + const preSlice = audioSamples.slice(0, hitIdx); + const postSlice = audioSamples + .slice(0, hitIdx) + .map((s) => ({ ...s, cts: s.cts - time })); + return [preSlice, postSlice]; +} diff --git a/packages/av-cliper/test-vite.config.ts b/packages/av-cliper/test-vite.config.ts index 0e131063..e5c85760 100644 --- a/packages/av-cliper/test-vite.config.ts +++ b/packages/av-cliper/test-vite.config.ts @@ -4,7 +4,7 @@ export default defineConfig({ test: { browser: { enabled: true, - name: 'edge', // browser name is required + name: 'chrome', // browser name is required headless: true, }, }, diff --git a/yarn.lock b/yarn.lock index 1eaf9ff8..a916d457 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1875,14 +1875,14 @@ loupe "^2.3.7" pretty-format "^29.7.0" -"@wdio/config@8.34.0": - version "8.34.0" - resolved "https://registry.npmmirror.com/@wdio/config/-/config-8.34.0.tgz#e4ee475ebed95a4d262b27204e2b854d6a508825" - integrity sha512-dtVGW/QEqM/WLUEZvca09y12L+hMZnzuwGuSzdG8B3wT6OaT+lSktU842HqHPC7OnZ27kRORhDJM6JLDy1T7dw== +"@wdio/config@8.36.1": + version "8.36.1" + resolved "https://registry.npmmirror.com/@wdio/config/-/config-8.36.1.tgz#150cd4477ef5e77c06fb752e0128342193ef1dc4" + integrity sha512-yCENnym0CrYuLKMJ3fv00WkjCR8QpPqVohGBkq5FvZOZpVJEpoG86Q8l4HtyRnd6ggMTKCA1vTQ/myhbPmZmaQ== dependencies: "@wdio/logger" "8.28.0" - "@wdio/types" "8.32.4" - "@wdio/utils" "8.33.1" + "@wdio/types" "8.36.1" + "@wdio/utils" "8.36.1" decamelize "^6.0.0" deepmerge-ts "^5.0.0" glob "^10.2.2" @@ -1910,21 +1910,21 @@ dependencies: "@types/node" "^20.1.0" -"@wdio/types@8.32.4": - version "8.32.4" - resolved "https://registry.npmmirror.com/@wdio/types/-/types-8.32.4.tgz#cc1965d5a802a09696b1ea9066e8b92f6977d51a" - integrity sha512-pDPGcCvq0MQF8u0sjw9m4aMI2gAKn6vphyBB2+1IxYriL777gbbxd7WQ+PygMBvYVprCYIkLPvhUFwF85WakmA== +"@wdio/types@8.36.1": + version "8.36.1" + resolved "https://registry.npmmirror.com/@wdio/types/-/types-8.36.1.tgz#5fa8dc05c0ce416b11b7a073fee0f319f533a70f" + integrity sha512-kKtyJbypasKo/VQuJ6dTQQwFtHE9qoygjoCZjrQCLGraRSjOEiqZHPR0497wbeCvcgHIYyImbmcylqZNGUE0CQ== dependencies: "@types/node" "^20.1.0" -"@wdio/utils@8.33.1": - version "8.33.1" - resolved "https://registry.npmmirror.com/@wdio/utils/-/utils-8.33.1.tgz#8ab0679d5214d9a4bea4aa198d3c1a806d386df0" - integrity sha512-W0ArrZbs4M23POv8+FPsgHDFxg+wwklfZgLSsjVq2kpCmBCfIPxKSAOgTo/XrcH4We/OnshgBzxLcI+BHDgi4w== +"@wdio/utils@8.36.1": + version "8.36.1" + resolved "https://registry.npmmirror.com/@wdio/utils/-/utils-8.36.1.tgz#b6b1e00c45963d1be8c7a9a65d90f7eeb65d6dbf" + integrity sha512-xmgPHU11/o9n2FeRmDFkPRC0okiwA1i2xOcR2c3aSpuk99XkAm9RaMn/6u9LFaqsCpgaVxazcYEGSceO7U4hZA== dependencies: "@puppeteer/browsers" "^1.6.0" "@wdio/logger" "8.28.0" - "@wdio/types" "8.32.4" + "@wdio/types" "8.36.1" decamelize "^6.0.0" deepmerge-ts "^5.1.0" edgedriver "^5.3.5" @@ -3585,10 +3585,10 @@ devtools-protocol@0.0.1147663: resolved "https://registry.npmmirror.com/devtools-protocol/-/devtools-protocol-0.0.1147663.tgz#4ec5610b39a6250d1f87e6b9c7e16688ed0ac78e" integrity sha512-hyWmRrexdhbZ1tcJUGpO95ivbRhWXz++F4Ko+n21AY5PNln2ovoJw+8ZMNDTtip+CNFQfrtLVh/w4009dXO/eQ== -devtools-protocol@^0.0.1263784: - version "0.0.1263784" - resolved "https://registry.npmmirror.com/devtools-protocol/-/devtools-protocol-0.0.1263784.tgz#fffc6860ac1afc46c4495e8b888ba50adb116bf3" - integrity sha512-k0SCZMwj587w4F8QYbP5iIbSonL6sd3q8aVJch036r9Tv2t9b5/Oq7AiJ/FJvRuORm/pJNXZtrdNNWlpRnl56A== +devtools-protocol@^0.0.1282316: + version "0.0.1282316" + resolved "https://registry.npmmirror.com/devtools-protocol/-/devtools-protocol-0.0.1282316.tgz#4b398549e48251a09bbad7056578f7ad376f9b81" + integrity sha512-i7eIqWdVxeXBY/M+v83yRkOV1sTHnr3XYiC0YNBivLIE6hBfE2H0c2o8VC5ynT44yjy+Ei0kLrBQFK/RUKaAHQ== dezalgo@^1.0.0: version "1.0.4" @@ -10015,40 +10015,40 @@ web-streams-polyfill@^3.0.3: resolved "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== -webdriver@8.34.0: - version "8.34.0" - resolved "https://registry.npmmirror.com/webdriver/-/webdriver-8.34.0.tgz#32509b5aeb63e574f053c5c6eacff7f78748c6eb" - integrity sha512-r8oe6tIm2qSdLNhzwVpVMWJvuoZTbeTZI21+EidqZtp6krlnJzd+hIRD5ojTm+yYi86JlRQnMMQ40LUBsAGVsQ== +webdriver@8.36.1: + version "8.36.1" + resolved "https://registry.npmmirror.com/webdriver/-/webdriver-8.36.1.tgz#4e57a9bc50a7cf1398955b8426c99ac2f6e1f36c" + integrity sha512-547RivYCHStVqtiGQBBcABAkzJbPnAWsxpXGzmj5KL+TOM2JF41N2iQRtUxXqr0jme1Nzzye7WS7Y7iSnK6i1g== dependencies: "@types/node" "^20.1.0" "@types/ws" "^8.5.3" - "@wdio/config" "8.34.0" + "@wdio/config" "8.36.1" "@wdio/logger" "8.28.0" "@wdio/protocols" "8.32.0" - "@wdio/types" "8.32.4" - "@wdio/utils" "8.33.1" + "@wdio/types" "8.36.1" + "@wdio/utils" "8.36.1" deepmerge-ts "^5.1.0" got "^12.6.1" ky "^0.33.0" ws "^8.8.0" -webdriverio@^8.34.0: - version "8.34.0" - resolved "https://registry.npmmirror.com/webdriverio/-/webdriverio-8.34.0.tgz#1e27a2607701205f23adb896d8df3fc4454bba6d" - integrity sha512-t9PJBcK4UANJzw8pr4GSrv4JxsrKHoozfScBCtcbQFpmvjhZZhjwZl4EsG89znMqIj44qh0nCOW6GnSZ1IGeBg== +webdriverio@^8.36.1: + version "8.36.1" + resolved "https://registry.npmmirror.com/webdriverio/-/webdriverio-8.36.1.tgz#67f6f5e7d383878910d51e8362b003bab45623c6" + integrity sha512-vzE09oFQeMbOYJ/75jZ13sDIljzC3HH7uoUJKAMAEtyrn/bu1F9Sg/4IDEsvQaRD3pz3ae6SkRld33lcQk6HJA== dependencies: "@types/node" "^20.1.0" - "@wdio/config" "8.34.0" + "@wdio/config" "8.36.1" "@wdio/logger" "8.28.0" "@wdio/protocols" "8.32.0" "@wdio/repl" "8.24.12" - "@wdio/types" "8.32.4" - "@wdio/utils" "8.33.1" + "@wdio/types" "8.36.1" + "@wdio/utils" "8.36.1" archiver "^7.0.0" aria-query "^5.0.0" css-shorthand-properties "^1.1.1" css-value "^0.0.1" - devtools-protocol "^0.0.1263784" + devtools-protocol "^0.0.1282316" grapheme-splitter "^1.0.2" import-meta-resolve "^4.0.0" is-plain-obj "^4.1.0" @@ -10060,7 +10060,7 @@ webdriverio@^8.34.0: resq "^1.9.1" rgb2hex "0.2.5" serialize-error "^11.0.1" - webdriver "8.34.0" + webdriver "8.36.1" webidl-conversions@^3.0.0: version "3.0.1"