diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..92cde390 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} \ No newline at end of file diff --git a/packages/av-canvas/demo/record-avcanvas.ts b/packages/av-canvas/demo/record-avcanvas.ts index 54bbf4ce..8632b5c0 100644 --- a/packages/av-canvas/demo/record-avcanvas.ts +++ b/packages/av-canvas/demo/record-avcanvas.ts @@ -1,20 +1,20 @@ -import { Combinator } from "@webav/av-cliper"; +import { Combinator } from '@webav/av-cliper'; import { AVCanvas, AudioSprite, TextSprite, ImgSprite, VideoSprite, -} from "../src/index"; -import { AVRecorder } from "@webav/av-recorder"; +} from '../src/index'; +import { AVRecorder } from '@webav/av-recorder'; (async () => { if (!(await Combinator.isSupported())) { - alert("Your browser does not support WebCodecs"); + alert('Your browser does not support WebCodecs'); } })(); -const avCvs = new AVCanvas(document.querySelector("#app") as HTMLElement, { - bgColor: "#333", +const avCvs = new AVCanvas(document.querySelector('#app') as HTMLElement, { + bgColor: '#333', resolution: { width: 1920, height: 1080, @@ -26,112 +26,112 @@ console.log({ avCvs }); // const is = new ImgSprite('img', 'https://neo-pages.bilibili.com/bbfe/neo/assets/img/neo-pages-overview.48f7bb81.png') // await avCvs.spriteManager.addSprite(is) })().catch(console.error); -document.querySelector("#userMedia")?.addEventListener("click", () => { +document.querySelector('#userMedia')?.addEventListener('click', () => { (async () => { const mediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true, }); - const vs = new VideoSprite("userMedia", mediaStream, { + const vs = new VideoSprite('userMedia', mediaStream, { audioCtx: avCvs.spriteManager.audioCtx, }); await avCvs.spriteManager.addSprite(vs); })().catch(console.error); }); -document.querySelector("#display")?.addEventListener("click", () => { +document.querySelector('#display')?.addEventListener('click', () => { (async () => { const mediaStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true, }); - const vs = new VideoSprite("display", mediaStream, { + const vs = new VideoSprite('display', mediaStream, { audioCtx: avCvs.spriteManager.audioCtx, }); await avCvs.spriteManager.addSprite(vs); })().catch(console.error); }); -document.querySelector("#localImg")?.addEventListener("click", () => { +document.querySelector('#localImg')?.addEventListener('click', () => { (async () => { const [imgFH] = await (window as any).showOpenFilePicker({ types: [ { - description: "Images", + description: 'Images', accept: { - "image/*": [".png", ".gif", ".jpeg", ".jpg"], + 'image/*': ['.png', '.gif', '.jpeg', '.jpg'], }, }, ], }); - const is = new ImgSprite("img", await imgFH.getFile()); + const is = new ImgSprite('img', await imgFH.getFile()); await avCvs.spriteManager.addSprite(is); })().catch(console.error); }); -document.querySelector("#localVideo")?.addEventListener("click", () => { +document.querySelector('#localVideo')?.addEventListener('click', () => { (async () => { const [imgFH] = await (window as any).showOpenFilePicker({ types: [ { - description: "Video", + description: 'Video', accept: { - "video/*": [".webm", ".mp4"], + 'video/*': ['.webm', '.mp4'], }, }, ], }); - const vs = new VideoSprite("vs", await imgFH.getFile(), { + const vs = new VideoSprite('vs', await imgFH.getFile(), { audioCtx: avCvs.spriteManager.audioCtx, }); await avCvs.spriteManager.addSprite(vs); })().catch(console.error); }); -document.querySelector("#localAudio")?.addEventListener("click", () => { +document.querySelector('#localAudio')?.addEventListener('click', () => { (async () => { const [imgFH] = await (window as any).showOpenFilePicker({ types: [ { - description: "Audio", + description: 'Audio', accept: { - "audio/*": [".mp3", ".wav", ".ogg"], + 'audio/*': ['.mp3', '.wav', '.ogg'], }, }, ], }); - const as = new AudioSprite("vs", await imgFH.getFile(), { + const as = new AudioSprite('vs', await imgFH.getFile(), { audioCtx: avCvs.spriteManager.audioCtx, }); await avCvs.spriteManager.addSprite(as); })().catch(console.error); }); -document.querySelector("#fontExamp")?.addEventListener("click", () => { +document.querySelector('#fontExamp')?.addEventListener('click', () => { (async () => { - const textSpr = new TextSprite("text", "示例文字"); + const textSpr = new TextSprite('text', '示例文字'); await avCvs.spriteManager.addSprite(textSpr); })().catch(console.error); }); let recorder: AVRecorder | null = null; -document.querySelector("#startRecod")?.addEventListener("click", () => { +document.querySelector('#startRecod')?.addEventListener('click', () => { (async () => { - const writer = await createFileWriter("mp4"); + const writer = await createFileWriter('mp4'); recorder = new AVRecorder(avCvs.captureStream(), { width: 1920, height: 1080, bitrate: 5e6, - audioCodec: "aac", + audioCodec: 'aac', }); await recorder.start(); recorder.outputStream?.pipeTo(writer).catch(console.error); })().catch(console.error); }); -document.querySelector("#stopRecod")?.addEventListener("click", () => { +document.querySelector('#stopRecod')?.addEventListener('click', () => { (async () => { await recorder?.stop(); - alert("save done"); + alert('save done'); })().catch(console.error); }); diff --git a/packages/av-canvas/src/__tests__/av-canvas.test.ts b/packages/av-canvas/src/__tests__/av-canvas.test.ts index e90b4936..d1b5ff6c 100644 --- a/packages/av-canvas/src/__tests__/av-canvas.test.ts +++ b/packages/av-canvas/src/__tests__/av-canvas.test.ts @@ -1,19 +1,19 @@ -import "./mock"; -import { beforeEach, expect, test, vi } from "vitest"; -import { AVCanvas } from "../av-canvas"; -import { createEl } from "../utils"; -import { VideoSprite } from "../sprites/video-sprite"; -import { crtMSEvt4Offset, cvsCaptureStreamMock, CvsElementMock } from "./mock"; +import './mock'; +import { beforeEach, expect, test, vi } from 'vitest'; +import { AVCanvas } from '../av-canvas'; +import { createEl } from '../utils'; +import { VideoSprite } from '../sprites/video-sprite'; +import { crtMSEvt4Offset, cvsCaptureStreamMock, CvsElementMock } from './mock'; function createAVCanvas(): { avCvs: AVCanvas; container: HTMLElement; } { - const container = createEl("div"); + const container = createEl('div'); return { avCvs: new AVCanvas(container, { resolution: { width: 100, height: 100 }, - bgColor: "#333", + bgColor: '#333', }), container, }; @@ -33,14 +33,14 @@ beforeEach(() => { container = d.container; }); -test("av-canvas create & destroy", () => { - const spyMngDestroy = vi.spyOn(avCvs.spriteManager, "destroy"); +test('av-canvas create & destroy', () => { + const spyMngDestroy = vi.spyOn(avCvs.spriteManager, 'destroy'); avCvs.destroy(); expect(spyMngDestroy).toBeCalled(); }); -test("init center the Sprite", async () => { - const vs = new VideoSprite("vs", new MediaStream()); +test('init center the Sprite', async () => { + const vs = new VideoSprite('vs', new MediaStream()); await vs.initReady; vs.rect.w = 80; vs.rect.h = 80; @@ -49,65 +49,65 @@ test("init center the Sprite", async () => { expect(vs.rect.y).toBe((100 - 80) / 2); }); -test("captureStream", () => { +test('captureStream', () => { const mockMS = new MediaStream(); cvsCaptureStreamMock.mockReturnValueOnce(mockMS); - vi.spyOn(mockMS, "getTracks").mockReturnValue(["mock-track"] as any); + vi.spyOn(mockMS, 'getTracks').mockReturnValue(['mock-track'] as any); const ms = avCvs.captureStream(); expect(ms).toBeInstanceOf(MediaStream); - expect(ms.addTrack).toBeCalledWith("mock-track"); + expect(ms.addTrack).toBeCalledWith('mock-track'); }); -test("activeSprite", async () => { - const vs = new VideoSprite("vs", new MediaStream()); +test('activeSprite', async () => { + const vs = new VideoSprite('vs', new MediaStream()); await vs.initReady; vs.rect.w = 80; vs.rect.h = 80; await avCvs.spriteManager.addSprite(vs); - const cvsEl = container.querySelector("canvas") as HTMLCanvasElement; - cvsEl.dispatchEvent(crtMSEvt4Offset("mousedown", 20, 20)); + const cvsEl = container.querySelector('canvas') as HTMLCanvasElement; + cvsEl.dispatchEvent(crtMSEvt4Offset('mousedown', 20, 20)); expect(avCvs.spriteManager.activeSprite).toBe(vs); - cvsEl.dispatchEvent(crtMSEvt4Offset("mousedown", 10, 10)); + cvsEl.dispatchEvent(crtMSEvt4Offset('mousedown', 10, 10)); // 命中 ctrls.lt expect(avCvs.spriteManager.activeSprite).toBe(vs); - cvsEl.dispatchEvent(crtMSEvt4Offset("mousedown", 0, 0)); + cvsEl.dispatchEvent(crtMSEvt4Offset('mousedown', 0, 0)); expect(avCvs.spriteManager.activeSprite).toBeNull(); }); -test("dynamicCusor", async () => { - const vs = new VideoSprite("vs", new MediaStream()); +test('dynamicCusor', async () => { + const vs = new VideoSprite('vs', new MediaStream()); await vs.initReady; vs.rect.w = 80; vs.rect.h = 80; await avCvs.spriteManager.addSprite(vs); - const cvsEl = container.querySelector("canvas") as HTMLCanvasElement; - cvsEl.dispatchEvent(crtMSEvt4Offset("mousedown", 20, 20)); - window.dispatchEvent(crtMSEvt4Offset("mouseup", 20, 20)); + const cvsEl = container.querySelector('canvas') as HTMLCanvasElement; + cvsEl.dispatchEvent(crtMSEvt4Offset('mousedown', 20, 20)); + window.dispatchEvent(crtMSEvt4Offset('mouseup', 20, 20)); expect(avCvs.spriteManager.activeSprite).toBe(vs); - expect(cvsEl.style.cursor).toBe("move"); + expect(cvsEl.style.cursor).toBe('move'); const { center, ctrls: { lt, rotate }, } = vs.rect; cvsEl.dispatchEvent( - crtMSEvt4Offset("mousemove", lt.x + center.x, lt.y + center.y), + crtMSEvt4Offset('mousemove', lt.x + center.x, lt.y + center.y), ); - expect(cvsEl.style.cursor).toBe("nwse-resize"); + expect(cvsEl.style.cursor).toBe('nwse-resize'); cvsEl.dispatchEvent( - crtMSEvt4Offset("mousemove", rotate.x + center.x, rotate.y + center.y), + crtMSEvt4Offset('mousemove', rotate.x + center.x, rotate.y + center.y), ); - expect(cvsEl.style.cursor).toBe("crosshair"); + expect(cvsEl.style.cursor).toBe('crosshair'); - cvsEl.dispatchEvent(crtMSEvt4Offset("mousemove", 0, 0)); - expect(cvsEl.style.cursor).toBe(""); + cvsEl.dispatchEvent(crtMSEvt4Offset('mousemove', 0, 0)); + expect(cvsEl.style.cursor).toBe(''); - cvsEl.dispatchEvent(crtMSEvt4Offset("mousemove", 20, 20)); - expect(cvsEl.style.cursor).toBe("move"); + cvsEl.dispatchEvent(crtMSEvt4Offset('mousemove', 20, 20)); + expect(cvsEl.style.cursor).toBe('move'); }); diff --git a/packages/av-canvas/src/__tests__/event-tool.test.ts b/packages/av-canvas/src/__tests__/event-tool.test.ts index 95d9fe49..cd51aee4 100644 --- a/packages/av-canvas/src/__tests__/event-tool.test.ts +++ b/packages/av-canvas/src/__tests__/event-tool.test.ts @@ -1,7 +1,7 @@ -import { expect, test, vi } from "vitest"; -import { EventTool } from "../event-tool"; +import { expect, test, vi } from 'vitest'; +import { EventTool } from '../event-tool'; -test("event-tool", () => { +test('event-tool', () => { const evtTool = new EventTool<{ a: (str: string) => void; b: () => void; @@ -9,19 +9,19 @@ test("event-tool", () => { const onA = vi.fn(); const onB = vi.fn(); - evtTool.on("a", onA); - const offB = evtTool.on("b", onB); + evtTool.on('a', onA); + const offB = evtTool.on('b', onB); offB(); - evtTool.emit("a", "test"); - evtTool.emit("b"); + evtTool.emit('a', 'test'); + evtTool.emit('b'); - expect(onA).toBeCalledWith("test"); + expect(onA).toBeCalledWith('test'); expect(onB).not.toBeCalled(); onA.mockClear(); evtTool.destroy(); - evtTool.emit("a", "test"); + evtTool.emit('a', 'test'); expect(onA).not.toBeCalled(); }); diff --git a/packages/av-canvas/src/__tests__/mock.ts b/packages/av-canvas/src/__tests__/mock.ts index f7d9aee8..cb0e6eab 100644 --- a/packages/av-canvas/src/__tests__/mock.ts +++ b/packages/av-canvas/src/__tests__/mock.ts @@ -1,4 +1,4 @@ -import { vi } from "vitest"; +import { vi } from 'vitest'; export const MediaStreamMock = { getTracks: vi.fn().mockReturnValue([]), @@ -52,21 +52,21 @@ Object.assign(global, { }), }); -vi.spyOn(HTMLVideoElement.prototype, "addEventListener").mockImplementation( +vi.spyOn(HTMLVideoElement.prototype, 'addEventListener').mockImplementation( (_, handler: any) => { handler(); }, ); -vi.spyOn(HTMLVideoElement.prototype, "play").mockImplementation(async () => { +vi.spyOn(HTMLVideoElement.prototype, 'play').mockImplementation(async () => { return await Promise.resolve(); }); -vi.spyOn(HTMLAudioElement.prototype, "play").mockImplementation(async () => { +vi.spyOn(HTMLAudioElement.prototype, 'play').mockImplementation(async () => { return await Promise.resolve(); }); -vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(() => { +vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockImplementation(() => { return { drawImage: vi.fn(), fillRect: vi.fn(), @@ -78,10 +78,10 @@ vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(() => { export const CvsElementMock = { clientWidth: vi - .spyOn(HTMLCanvasElement.prototype, "clientWidth", "get") + .spyOn(HTMLCanvasElement.prototype, 'clientWidth', 'get') .mockImplementation(() => 0), clientHeight: vi - .spyOn(HTMLCanvasElement.prototype, "clientHeight", "get") + .spyOn(HTMLCanvasElement.prototype, 'clientHeight', 'get') .mockImplementation(() => 0), }; @@ -92,7 +92,7 @@ Object.assign(HTMLCanvasElement.prototype, { export const getBoundingClientRectMock = vi.spyOn( HTMLElement.prototype, - "getBoundingClientRect", + 'getBoundingClientRect', ); export const createObjectURLMock = (URL.createObjectURL = vi.fn()); @@ -111,8 +111,8 @@ export function crtMSEvt4Offset( offsetY: number, ): MouseEvent { const evt = new MouseEvent(evtName); - vi.spyOn(evt, "offsetX", "get").mockImplementation(() => offsetX); - vi.spyOn(evt, "offsetY", "get").mockImplementation(() => offsetY); + vi.spyOn(evt, 'offsetX', 'get').mockImplementation(() => offsetX); + vi.spyOn(evt, 'offsetY', 'get').mockImplementation(() => offsetY); return evt; } diff --git a/packages/av-canvas/src/av-canvas.ts b/packages/av-canvas/src/av-canvas.ts index 91a93bd7..828752ba 100644 --- a/packages/av-canvas/src/av-canvas.ts +++ b/packages/av-canvas/src/av-canvas.ts @@ -1,12 +1,12 @@ -import { Log, Rect, TCtrlKey } from "@webav/av-cliper"; -import { renderCtrls } from "./sprites/render-ctrl"; -import { ESpriteManagerEvt, SpriteManager } from "./sprites/sprite-manager"; -import { activeSprite, draggabelSprite } from "./sprites/sprite-op"; -import { IResolution } from "./types"; -import { createEl } from "./utils"; +import { Log, Rect, TCtrlKey } from '@webav/av-cliper'; +import { renderCtrls } from './sprites/render-ctrl'; +import { ESpriteManagerEvt, SpriteManager } from './sprites/sprite-manager'; +import { activeSprite, draggabelSprite } from './sprites/sprite-op'; +import { IResolution } from './types'; +import { createEl } from './utils'; function createInitCvsEl(resolution: IResolution): HTMLCanvasElement { - const cvsEl = createEl("canvas") as HTMLCanvasElement; + const cvsEl = createEl('canvas') as HTMLCanvasElement; cvsEl.style.cssText = ` width: 100%; height: 100%; @@ -36,8 +36,8 @@ export class AVCanvas { }, ) { this.#cvsEl = createInitCvsEl(opts.resolution); - const ctx = this.#cvsEl.getContext("2d", { alpha: false }); - if (ctx == null) throw Error("canvas context is null"); + const ctx = this.#cvsEl.getContext('2d', { alpha: false }); + if (ctx == null) throw Error('canvas context is null'); this.#cvsCtx = ctx; container.appendChild(this.#cvsEl); @@ -87,8 +87,8 @@ export class AVCanvas { } captureStream(): MediaStream { - if (this.spriteManager.audioCtx.state === "suspended") { - Log.info("AVCanvas.captureStream resume AudioContext"); + if (this.spriteManager.audioCtx.state === 'suspended') { + Log.info('AVCanvas.captureStream resume AudioContext'); this.spriteManager.audioCtx.resume().catch(Log.error); } @@ -101,7 +101,7 @@ export class AVCanvas { ms.addTrack(t); }); Log.info( - "AVCanvas.captureStream, tracks:", + 'AVCanvas.captureStream, tracks:', ms.getTracks().map((t) => t.kind), ); return ms; @@ -139,7 +139,7 @@ function dynamicCusor( let actSpr = sprMng.activeSprite; sprMng.on(ESpriteManagerEvt.ActiveSpriteChange, (s) => { actSpr = s; - if (s == null) cvsStyle.cursor = ""; + if (s == null) cvsStyle.cursor = ''; }); // 鼠标按下时,在操作过程中,不需要变换鼠标样式 let isMSDown = false; @@ -149,8 +149,8 @@ function dynamicCusor( const ofx = offsetX / cvsRatio.w; const ofy = offsetY / cvsRatio.h; // 直接选中 sprite 时,需要改变鼠标样式为 move - if (actSpr?.rect.checkHit(ofx, ofy) === true && cvsStyle.cursor === "") { - cvsStyle.cursor = "move"; + if (actSpr?.rect.checkHit(ofx, ofy) === true && cvsStyle.cursor === '') { + cvsStyle.cursor = 'move'; } }; const onWindowUp = (): void => { @@ -159,14 +159,14 @@ function dynamicCusor( // 八个 ctrl 点位对应的鼠标样式,构成循环 const curStyles = [ - "ns-resize", - "nesw-resize", - "ew-resize", - "nwse-resize", - "ns-resize", - "nesw-resize", - "ew-resize", - "nwse-resize", + 'ns-resize', + 'nesw-resize', + 'ew-resize', + 'nwse-resize', + 'ns-resize', + 'nesw-resize', + 'ew-resize', + 'nwse-resize', ]; const curInitIdx = { t: 0, rt: 1, r: 2, rb: 3, b: 4, lb: 5, l: 6, lt: 7 }; @@ -182,8 +182,8 @@ function dynamicCusor( ) as [TCtrlKey, Rect]) ?? []; if (ctrlKey != null) { - if (ctrlKey === "rotate") { - cvsStyle.cursor = "crosshair"; + if (ctrlKey === 'rotate') { + cvsStyle.cursor = 'crosshair'; return; } // 旋转后,控制点的箭头指向也需要修正 @@ -198,21 +198,21 @@ function dynamicCusor( return; } if (actSpr.rect.checkHit(ofx, ofy)) { - cvsStyle.cursor = "move"; + cvsStyle.cursor = 'move'; return; } // 未命中 ctrls、sprite,重置为默认鼠标样式 - cvsStyle.cursor = ""; + cvsStyle.cursor = ''; }; - cvsEl.addEventListener("mousemove", onMove); - cvsEl.addEventListener("mousedown", onDown); - window.addEventListener("mouseup", onWindowUp); + cvsEl.addEventListener('mousemove', onMove); + cvsEl.addEventListener('mousedown', onDown); + window.addEventListener('mouseup', onWindowUp); return () => { observer.disconnect(); - cvsEl.removeEventListener("mousemove", onMove); - cvsEl.removeEventListener("mousedown", onDown); - window.removeEventListener("mouseup", onWindowUp); + cvsEl.removeEventListener('mousemove', onMove); + cvsEl.removeEventListener('mousedown', onDown); + window.removeEventListener('mouseup', onWindowUp); }; } diff --git a/packages/av-canvas/src/index.ts b/packages/av-canvas/src/index.ts index 7edcb1ec..9b2ab2af 100644 --- a/packages/av-canvas/src/index.ts +++ b/packages/av-canvas/src/index.ts @@ -1,6 +1,6 @@ -export { AVCanvas } from "./av-canvas"; +export { AVCanvas } from './av-canvas'; -export { AudioSprite } from "./sprites/audio-sprite"; -export { VideoSprite } from "./sprites/video-sprite"; -export { TextSprite } from "./sprites/text-sprite"; -export { ImgSprite } from "./sprites/img-sprite"; +export { AudioSprite } from './sprites/audio-sprite'; +export { VideoSprite } from './sprites/video-sprite'; +export { TextSprite } from './sprites/text-sprite'; +export { ImgSprite } from './sprites/img-sprite'; diff --git a/packages/av-canvas/src/sprites/__tests__/audio-sprite.test.ts b/packages/av-canvas/src/sprites/__tests__/audio-sprite.test.ts index 6141e2aa..0aa838cc 100644 --- a/packages/av-canvas/src/sprites/__tests__/audio-sprite.test.ts +++ b/packages/av-canvas/src/sprites/__tests__/audio-sprite.test.ts @@ -1,15 +1,15 @@ -import { createObjectURLMock, revokeObjectURLMock } from "../../__tests__/mock"; -import { beforeEach, expect, test } from "vitest"; -import { AudioSprite } from "../audio-sprite"; +import { createObjectURLMock, revokeObjectURLMock } from '../../__tests__/mock'; +import { beforeEach, expect, test } from 'vitest'; +import { AudioSprite } from '../audio-sprite'; beforeEach(() => { createObjectURLMock.mockReset(); revokeObjectURLMock.mockReset(); }); -test("create AudioSprite", () => { - createObjectURLMock.mockReturnValueOnce("mock-audio-src"); - const as = new AudioSprite("as", { type: "audio/mpeg" } as unknown as File, { +test('create AudioSprite', () => { + createObjectURLMock.mockReturnValueOnce('mock-audio-src'); + const as = new AudioSprite('as', { type: 'audio/mpeg' } as unknown as File, { audioCtx: new AudioContext(), }); expect(as.audioNode).not.toBeNull(); diff --git a/packages/av-canvas/src/sprites/__tests__/sprite-manager.test.ts b/packages/av-canvas/src/sprites/__tests__/sprite-manager.test.ts index b9c06ef0..e2b89e52 100644 --- a/packages/av-canvas/src/sprites/__tests__/sprite-manager.test.ts +++ b/packages/av-canvas/src/sprites/__tests__/sprite-manager.test.ts @@ -1,38 +1,38 @@ -import { vi, expect, test } from "vitest"; -import "../../__tests__/mock"; -import { SpriteManager } from "../sprite-manager"; -import { VideoSprite } from "../video-sprite"; +import { vi, expect, test } from 'vitest'; +import '../../__tests__/mock'; +import { SpriteManager } from '../sprite-manager'; +import { VideoSprite } from '../video-sprite'; -test("addSprite", async () => { +test('addSprite', async () => { const sprMng = new SpriteManager(); - const vs1 = new VideoSprite("vs1", new MediaStream()); + const vs1 = new VideoSprite('vs1', new MediaStream()); vs1.zIndex = 10; - const vs2 = new VideoSprite("vs2", new MediaStream()); + const vs2 = new VideoSprite('vs2', new MediaStream()); vs1.zIndex = 1; await sprMng.addSprite(vs1); await sprMng.addSprite(vs2); - expect(sprMng.getSprites().map((it) => it.name)).toEqual(["vs2", "vs1"]); + expect(sprMng.getSprites().map((it) => it.name)).toEqual(['vs2', 'vs1']); }); -test("removeSprite", async () => { +test('removeSprite', async () => { const sprMng = new SpriteManager(); - const vs1 = new VideoSprite("vs1", new MediaStream()); + const vs1 = new VideoSprite('vs1', new MediaStream()); await sprMng.addSprite(vs1); - const spyDestroy = vi.spyOn(vs1, "destroy"); + const spyDestroy = vi.spyOn(vs1, 'destroy'); sprMng.removeSprite(vs1); expect(spyDestroy).toBeCalled(); }); -test("spriteManager destroy", async () => { +test('spriteManager destroy', async () => { const sprMng = new SpriteManager(); - const vs1 = new VideoSprite("vs1", new MediaStream()); - const spyDestroy = vi.spyOn(vs1, "destroy"); + const vs1 = new VideoSprite('vs1', new MediaStream()); + const spyDestroy = vi.spyOn(vs1, 'destroy'); await sprMng.addSprite(vs1); sprMng.destroy(); diff --git a/packages/av-canvas/src/sprites/__tests__/sprite-op.test.ts b/packages/av-canvas/src/sprites/__tests__/sprite-op.test.ts index f6ac2909..5ac7a5c9 100644 --- a/packages/av-canvas/src/sprites/__tests__/sprite-op.test.ts +++ b/packages/av-canvas/src/sprites/__tests__/sprite-op.test.ts @@ -1,11 +1,11 @@ -import { beforeEach, describe, expect, test, vi } from "vitest"; -import { crtMSEvt4Offset } from "../../__tests__/mock"; -import { SpriteManager } from "../sprite-manager"; -import { createEl } from "../../utils"; -import { draggabelSprite } from "../sprite-op"; -import { VideoSprite } from "../video-sprite"; - -let cvsEl = createEl("canvas") as HTMLCanvasElement; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { crtMSEvt4Offset } from '../../__tests__/mock'; +import { SpriteManager } from '../sprite-manager'; +import { createEl } from '../../utils'; +import { draggabelSprite } from '../sprite-op'; +import { VideoSprite } from '../video-sprite'; + +let cvsEl = createEl('canvas') as HTMLCanvasElement; const cvsRatio = { w: 1, h: 1, @@ -13,57 +13,57 @@ const cvsRatio = { let sprMng = new SpriteManager(); beforeEach(() => { sprMng = new SpriteManager(); - cvsEl = createEl("canvas") as HTMLCanvasElement; - vi.spyOn(cvsEl, "clientWidth", "get").mockImplementation(() => 900); - vi.spyOn(cvsEl, "clientHeight", "get").mockImplementation(() => 500); + cvsEl = createEl('canvas') as HTMLCanvasElement; + vi.spyOn(cvsEl, 'clientWidth', 'get').mockImplementation(() => 900); + vi.spyOn(cvsEl, 'clientHeight', 'get').mockImplementation(() => 500); cvsEl.width = 1920; cvsEl.height = 1080; cvsRatio.w = 900 / 1920; cvsRatio.h = 500 / 1080; }); -describe("draggabelSprite", () => { - test("canvas on mousedown", () => { - const spyAEL = vi.spyOn(cvsEl, "addEventListener"); - const spyREL = vi.spyOn(cvsEl, "removeEventListener"); +describe('draggabelSprite', () => { + test('canvas on mousedown', () => { + const spyAEL = vi.spyOn(cvsEl, 'addEventListener'); + const spyREL = vi.spyOn(cvsEl, 'removeEventListener'); const clear = draggabelSprite(cvsEl, sprMng); - expect(spyAEL).toBeCalledWith("mousedown", expect.any(Function)); + expect(spyAEL).toBeCalledWith('mousedown', expect.any(Function)); expect(clear).toBeInstanceOf(Function); clear(); - expect(spyREL).toBeCalledWith("mousedown", expect.any(Function)); + expect(spyREL).toBeCalledWith('mousedown', expect.any(Function)); }); - test("window on mouse event", async () => { - const spyAEL = vi.spyOn(window, "addEventListener"); - const spyREL = vi.spyOn(window, "removeEventListener"); - const vs = new VideoSprite("vs", new MediaStream()); - vi.spyOn(vs.rect, "checkHit").mockReturnValue(true); + test('window on mouse event', async () => { + const spyAEL = vi.spyOn(window, 'addEventListener'); + const spyREL = vi.spyOn(window, 'removeEventListener'); + const vs = new VideoSprite('vs', new MediaStream()); + vi.spyOn(vs.rect, 'checkHit').mockReturnValue(true); await sprMng.addSprite(vs); sprMng.activeSprite = vs; const clear = draggabelSprite(cvsEl, sprMng); - cvsEl.dispatchEvent(new MouseEvent("mousedown")); + cvsEl.dispatchEvent(new MouseEvent('mousedown')); expect(spyAEL).toBeCalledTimes(2); expect(spyAEL).toHaveBeenNthCalledWith( 1, - "mousemove", + 'mousemove', expect.any(Function), ); - expect(spyAEL).toHaveBeenNthCalledWith(2, "mouseup", expect.any(Function)); + expect(spyAEL).toHaveBeenNthCalledWith(2, 'mouseup', expect.any(Function)); clear(); expect(spyREL).toHaveBeenNthCalledWith( 1, - "mousemove", + 'mousemove', expect.any(Function), ); - expect(spyREL).toHaveBeenNthCalledWith(2, "mouseup", expect.any(Function)); + expect(spyREL).toHaveBeenNthCalledWith(2, 'mouseup', expect.any(Function)); }); - test("move sprite", async () => { - const vs = new VideoSprite("vs", new MediaStream()); + test('move sprite', async () => { + const vs = new VideoSprite('vs', new MediaStream()); vs.rect.w = 100; vs.rect.h = 100; @@ -71,10 +71,10 @@ describe("draggabelSprite", () => { sprMng.activeSprite = vs; const clear = draggabelSprite(cvsEl, sprMng); - cvsEl.dispatchEvent(crtMSEvt4Offset("mousedown", 50, 50)); + cvsEl.dispatchEvent(crtMSEvt4Offset('mousedown', 50, 50)); window.dispatchEvent( - new MouseEvent("mousemove", { + new MouseEvent('mousemove', { clientX: 100, clientY: 100, }), @@ -84,7 +84,7 @@ describe("draggabelSprite", () => { // 鼠标移动超出边界 window.dispatchEvent( - new MouseEvent("mousemove", { + new MouseEvent('mousemove', { clientX: 10000, clientY: 10000, }), @@ -97,9 +97,9 @@ describe("draggabelSprite", () => { }); }); -describe("scale sprite", () => { - test("drag right ctrl", async () => { - const vs = new VideoSprite("vs", new MediaStream()); +describe('scale sprite', () => { + test('drag right ctrl', async () => { + const vs = new VideoSprite('vs', new MediaStream()); await sprMng.addSprite(vs); sprMng.activeSprite = vs; vs.rect.w = 100; @@ -107,16 +107,16 @@ describe("scale sprite", () => { // 激活 sprite const clear = draggabelSprite(cvsEl, sprMng); - cvsEl.dispatchEvent(crtMSEvt4Offset("mousedown", 0, 0)); + cvsEl.dispatchEvent(crtMSEvt4Offset('mousedown', 0, 0)); expect(sprMng.activeSprite).toBe(vs); - window.dispatchEvent(new MouseEvent("mouseup")); + window.dispatchEvent(new MouseEvent('mouseup')); // 命中 right ctrl cvsEl.dispatchEvent( - crtMSEvt4Offset("mousedown", 100 * cvsRatio.w, 50 * cvsRatio.h), + crtMSEvt4Offset('mousedown', 100 * cvsRatio.w, 50 * cvsRatio.h), ); window.dispatchEvent( - new MouseEvent("mousemove", { + new MouseEvent('mousemove', { clientX: 100, clientY: 100, }), @@ -127,8 +127,8 @@ describe("scale sprite", () => { clear(); }); - test("drag rb(bottom right) ctrl", async () => { - const vs = new VideoSprite("vs", new MediaStream()); + test('drag rb(bottom right) ctrl', async () => { + const vs = new VideoSprite('vs', new MediaStream()); await sprMng.addSprite(vs); sprMng.activeSprite = vs; vs.rect.w = 100; @@ -136,16 +136,16 @@ describe("scale sprite", () => { // 激活 sprite const clear = draggabelSprite(cvsEl, sprMng); - cvsEl.dispatchEvent(crtMSEvt4Offset("mousedown", 0, 0)); + cvsEl.dispatchEvent(crtMSEvt4Offset('mousedown', 0, 0)); expect(sprMng.activeSprite).toBe(vs); - window.dispatchEvent(new MouseEvent("mouseup")); + window.dispatchEvent(new MouseEvent('mouseup')); // 命中 right ctrl cvsEl.dispatchEvent( - crtMSEvt4Offset("mousedown", 100 * cvsRatio.w, 100 * cvsRatio.h), + crtMSEvt4Offset('mousedown', 100 * cvsRatio.w, 100 * cvsRatio.h), ); window.dispatchEvent( - new MouseEvent("mousemove", { + new MouseEvent('mousemove', { clientX: 100, clientY: 100, }), @@ -156,9 +156,9 @@ describe("scale sprite", () => { }); }); -describe("rotate sprite", () => { - test("rotate sprite", async () => { - const vs = new VideoSprite("vs", new MediaStream()); +describe('rotate sprite', () => { + test('rotate sprite', async () => { + const vs = new VideoSprite('vs', new MediaStream()); await sprMng.addSprite(vs); sprMng.activeSprite = vs; vs.rect.w = 100; @@ -166,10 +166,10 @@ describe("rotate sprite", () => { // 激活 sprite const clear = draggabelSprite(cvsEl, sprMng); - cvsEl.dispatchEvent(crtMSEvt4Offset("mousedown", 0, 0)); + cvsEl.dispatchEvent(crtMSEvt4Offset('mousedown', 0, 0)); expect(sprMng.activeSprite).toBe(vs); - window.dispatchEvent(new MouseEvent("mouseup")); + window.dispatchEvent(new MouseEvent('mouseup')); // 命中 rotate ctrl const { center, @@ -177,13 +177,13 @@ describe("rotate sprite", () => { } = vs.rect; cvsEl.dispatchEvent( crtMSEvt4Offset( - "mousedown", + 'mousedown', (rotate.x + center.x) * cvsRatio.w, (rotate.y + center.y) * cvsRatio.h, ), ); window.dispatchEvent( - new MouseEvent("mousemove", { + new MouseEvent('mousemove', { clientX: 100, clientY: 100, }), @@ -191,7 +191,7 @@ describe("rotate sprite", () => { expect(vs.rect.angle).toMatchSnapshot(); window.dispatchEvent( - new MouseEvent("mousemove", { + new MouseEvent('mousemove', { clientX: 100, clientY: 200, }), diff --git a/packages/av-canvas/src/sprites/__tests__/text-sprite.test.ts b/packages/av-canvas/src/sprites/__tests__/text-sprite.test.ts index 5636c799..c3363404 100644 --- a/packages/av-canvas/src/sprites/__tests__/text-sprite.test.ts +++ b/packages/av-canvas/src/sprites/__tests__/text-sprite.test.ts @@ -1,6 +1,6 @@ -import { getBoundingClientRectMock } from "../../__tests__/mock"; -import { vi, test, expect, beforeEach, afterEach } from "vitest"; -import { TextSprite } from "../text-sprite"; +import { getBoundingClientRectMock } from '../../__tests__/mock'; +import { vi, test, expect, beforeEach, afterEach } from 'vitest'; +import { TextSprite } from '../text-sprite'; beforeEach(() => { getBoundingClientRectMock.mockImplementation(() => { @@ -15,8 +15,8 @@ afterEach(() => { getBoundingClientRectMock.mockRestore(); }); -test("font-sprite", () => { - const textSpr = new TextSprite("text", "示例文本"); +test('font-sprite', () => { + const textSpr = new TextSprite('text', '示例文本'); const mockCtx = { drawImage: vi.fn(), rotate: vi.fn(), diff --git a/packages/av-canvas/src/sprites/__tests__/video-sprite.test.ts b/packages/av-canvas/src/sprites/__tests__/video-sprite.test.ts index e56a217c..1c2bde76 100644 --- a/packages/av-canvas/src/sprites/__tests__/video-sprite.test.ts +++ b/packages/av-canvas/src/sprites/__tests__/video-sprite.test.ts @@ -2,9 +2,9 @@ import { createObjectURLMock, MediaStreamMock, revokeObjectURLMock, -} from "../../__tests__/mock"; -import { vi, test, expect, beforeAll, afterAll } from "vitest"; -import { VideoSprite } from "../video-sprite"; +} from '../../__tests__/mock'; +import { vi, test, expect, beforeAll, afterAll } from 'vitest'; +import { VideoSprite } from '../video-sprite'; beforeAll(() => { createObjectURLMock.mockClear(); @@ -15,8 +15,8 @@ afterAll(() => { MediaStreamMock.getAudioTracks.mockRestore(); }); -test("VideoSprite render", async () => { - const vs = new VideoSprite("vs", new MediaStream()); +test('VideoSprite render', async () => { + const vs = new VideoSprite('vs', new MediaStream()); await vs.initReady; vs.rect.w = 100; vs.rect.h = 100; @@ -38,25 +38,25 @@ test("VideoSprite render", async () => { expect(mockCtx.setTransform).toBeCalledWith(1, 0, 0, 1, 50, 50); }); -test("VideoSprite destroy", async () => { - const vs = new VideoSprite("vs", new MediaStream()); +test('VideoSprite destroy', async () => { + const vs = new VideoSprite('vs', new MediaStream()); await vs.initReady; - const spyRM = vi.spyOn(HTMLVideoElement.prototype, "remove"); + const spyRM = vi.spyOn(HTMLVideoElement.prototype, 'remove'); vs.destroy(); expect(spyRM).toBeCalledTimes(1); }); -test("VideoSprite default volume", async () => { - const vs = new VideoSprite("vs", new MediaStream()); +test('VideoSprite default volume', async () => { + const vs = new VideoSprite('vs', new MediaStream()); await vs.initReady; expect(vs.volume).toBe(0); vs.volume = 1; expect(vs.volume).toBe(0); }); -test("VideoSprite audio ctrl", async () => { +test('VideoSprite audio ctrl', async () => { const ms = new MediaStream(); - const vs = new VideoSprite("vs", ms, { + const vs = new VideoSprite('vs', ms, { audioCtx: new AudioContext(), }); await vs.initReady; @@ -66,11 +66,11 @@ test("VideoSprite audio ctrl", async () => { expect(vs.volume).toBe(1); }); -test("create VideoSprite with file", async () => { - createObjectURLMock.mockReturnValueOnce("mock-video-src"); +test('create VideoSprite with file', async () => { + createObjectURLMock.mockReturnValueOnce('mock-video-src'); const vs = new VideoSprite( - "vs-file", - { type: "video/mp4" } as unknown as File, + 'vs-file', + { type: 'video/mp4' } as unknown as File, { audioCtx: new AudioContext() }, ); await vs.initReady; diff --git a/packages/av-canvas/src/sprites/audio-sprite.ts b/packages/av-canvas/src/sprites/audio-sprite.ts index 3bc14905..96bcd930 100644 --- a/packages/av-canvas/src/sprites/audio-sprite.ts +++ b/packages/av-canvas/src/sprites/audio-sprite.ts @@ -1,20 +1,20 @@ -import { createEl } from "../utils"; -import { BaseSprite } from "@webav/av-cliper"; +import { createEl } from '../utils'; +import { BaseSprite } from '@webav/av-cliper'; interface IAudioSpriteOpts { audioCtx?: AudioContext; } export class AudioSprite extends BaseSprite { - #audioEl = createEl("audio") as HTMLAudioElement; + #audioEl = createEl('audio') as HTMLAudioElement; // 音频节点没有可视内容 readonly visible = false; constructor(name: string, source: File, opts: IAudioSpriteOpts = {}) { super(name); - if (!["audio/mpeg", "audio/ogg", "audio/wav"].includes(source.type)) - throw new Error("Unsupport audio format"); + if (!['audio/mpeg', 'audio/ogg', 'audio/wav'].includes(source.type)) + throw new Error('Unsupport audio format'); if (opts.audioCtx != null) { this.audioNode = opts.audioCtx.createGain(); diff --git a/packages/av-canvas/src/sprites/img-sprite.ts b/packages/av-canvas/src/sprites/img-sprite.ts index a08f1627..1117f76d 100644 --- a/packages/av-canvas/src/sprites/img-sprite.ts +++ b/packages/av-canvas/src/sprites/img-sprite.ts @@ -1,4 +1,4 @@ -import { BaseSprite } from "@webav/av-cliper"; +import { BaseSprite } from '@webav/av-cliper'; export class ImgSprite extends BaseSprite { #img: HTMLImageElement = new Image(); @@ -9,14 +9,14 @@ export class ImgSprite extends BaseSprite { if ( source instanceof File && ![ - "image/png", - "image/jpg", - "image/jpeg", - "image/bmp", - "image/gif", + 'image/png', + 'image/jpg', + 'image/jpeg', + 'image/bmp', + 'image/gif', ].includes(source.type) ) - throw Error("Unsupport image format"); + throw Error('Unsupport image format'); this.initReady = this.#init(source).catch(console.error); } diff --git a/packages/av-canvas/src/sprites/render-ctrl.ts b/packages/av-canvas/src/sprites/render-ctrl.ts index f8eec3b0..c9c98ae6 100644 --- a/packages/av-canvas/src/sprites/render-ctrl.ts +++ b/packages/av-canvas/src/sprites/render-ctrl.ts @@ -1,7 +1,7 @@ -import { ICvsRatio } from "../types"; -import { createEl } from "../utils"; -import { BaseSprite, CTRL_KEYS, TCtrlKey } from "@webav/av-cliper"; -import { ESpriteManagerEvt, SpriteManager } from "./sprite-manager"; +import { ICvsRatio } from '../types'; +import { createEl } from '../utils'; +import { BaseSprite, CTRL_KEYS, TCtrlKey } from '@webav/av-cliper'; +import { ESpriteManagerEvt, SpriteManager } from './sprite-manager'; export function renderCtrls( container: HTMLElement, @@ -26,11 +26,11 @@ export function renderCtrls( const { rectEl, ctrlsEl } = createRectAndCtrlEl(container); const offSprChange = sprMng.on(ESpriteManagerEvt.ActiveSpriteChange, (s) => { if (s == null) { - rectEl.style.display = "none"; + rectEl.style.display = 'none'; return; } syncCtrlElPos(s, rectEl, ctrlsEl, cvsRatio); - rectEl.style.display = ""; + rectEl.style.display = ''; }); let isDown = false; @@ -47,17 +47,17 @@ export function renderCtrls( syncCtrlElPos(sprMng.activeSprite, rectEl, ctrlsEl, cvsRatio); }; - cvsEl.addEventListener("mousedown", onDown); - window.addEventListener("mouseup", onWinowUp); - window.addEventListener("mousemove", onMove); + cvsEl.addEventListener('mousedown', onDown); + window.addEventListener('mouseup', onWinowUp); + window.addEventListener('mousemove', onMove); return () => { observer.disconnect(); offSprChange(); rectEl.remove(); - cvsEl.removeEventListener("mousedown", onDown); - window.removeEventListener("mouseup", onWinowUp); - window.removeEventListener("mousemove", onMove); + cvsEl.removeEventListener('mousedown', onDown); + window.removeEventListener('mouseup', onWinowUp); + window.removeEventListener('mousemove', onMove); }; } @@ -65,7 +65,7 @@ function createRectAndCtrlEl(container: HTMLElement): { rectEl: HTMLElement; ctrlsEl: Record; } { - const rectEl = createEl("div"); + const rectEl = createEl('div'); rectEl.style.cssText = ` position: absolute; pointer-events: none; @@ -75,7 +75,7 @@ function createRectAndCtrlEl(container: HTMLElement): { `; const ctrlsEl = Object.fromEntries( CTRL_KEYS.map((k) => { - const d = createEl("div"); + const d = createEl('div'); d.style.cssText = ` position: absolute; border: 1px solid #3ee; @@ -112,8 +112,8 @@ function syncCtrlElPos( Object.entries(ctrls).forEach(([k, { x, y, w, h }]) => { // ctrl 是相对中心点定位的 Object.assign(ctrlsEl[k as TCtrlKey].style, { - left: "50%", - top: "50%", + left: '50%', + top: '50%', width: `${w * cvsRatio.w}px`, height: `${h * cvsRatio.h}px`, // border 1px, 所以要 -1 diff --git a/packages/av-canvas/src/sprites/sprite-manager.ts b/packages/av-canvas/src/sprites/sprite-manager.ts index 564554db..a1372ddf 100644 --- a/packages/av-canvas/src/sprites/sprite-manager.ts +++ b/packages/av-canvas/src/sprites/sprite-manager.ts @@ -1,9 +1,9 @@ -import { EventTool } from "../event-tool"; -import { BaseSprite } from "@webav/av-cliper"; +import { EventTool } from '../event-tool'; +import { BaseSprite } from '@webav/av-cliper'; export enum ESpriteManagerEvt { - ActiveSpriteChange = "activeSpriteChange", - AddSprite = "addSprite", + ActiveSpriteChange = 'activeSpriteChange', + AddSprite = 'addSprite', } export class SpriteManager { diff --git a/packages/av-canvas/src/sprites/sprite-op.ts b/packages/av-canvas/src/sprites/sprite-op.ts index 6a2c38b3..f68756cd 100644 --- a/packages/av-canvas/src/sprites/sprite-op.ts +++ b/packages/av-canvas/src/sprites/sprite-op.ts @@ -1,6 +1,6 @@ -import { SpriteManager } from "./sprite-manager"; -import { ICvsRatio, IPoint } from "../types"; -import { BaseSprite, Rect, TCtrlKey } from "@webav/av-cliper"; +import { SpriteManager } from './sprite-manager'; +import { ICvsRatio, IPoint } from '../types'; +import { BaseSprite, Rect, TCtrlKey } from '@webav/av-cliper'; /** * 鼠标点击,激活 sprite @@ -40,11 +40,11 @@ export function activeSprite( .find((s) => s.rect.checkHit(ofx, ofy)) ?? null; }; - cvsEl.addEventListener("mousedown", onCvsMouseDown); + cvsEl.addEventListener('mousedown', onCvsMouseDown); return () => { observer.disconnect(); - cvsEl.removeEventListener("mousedown", onCvsMouseDown); + cvsEl.removeEventListener('mousedown', onCvsMouseDown); }; } @@ -69,7 +69,7 @@ export function draggabelSprite( let startX = 0; let startY = 0; let startRect: Rect | null = null; - let mvLimit: Record<"xl" | "xr" | "yt" | "yb", number> | null = null; + let mvLimit: Record<'xl' | 'xr' | 'yt' | 'yb', number> | null = null; let hitSpr: BaseSprite | null = null; // sprMng.activeSprite 在 av-canvas.ts -> activeSprite 中被赋值 @@ -105,8 +105,8 @@ export function draggabelSprite( startX = clientX; startY = clientY; - window.addEventListener("mousemove", onMouseMove); - window.addEventListener("mouseup", clearWindowEvt); + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', clearWindowEvt); }; const onMouseMove = (evt: MouseEvent): void => { @@ -125,17 +125,17 @@ export function draggabelSprite( hitSpr.rect.y = newY; }; - cvsEl.addEventListener("mousedown", onCvsMouseDown); + cvsEl.addEventListener('mousedown', onCvsMouseDown); const clearWindowEvt = (): void => { - window.removeEventListener("mousemove", onMouseMove); - window.removeEventListener("mouseup", clearWindowEvt); + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', clearWindowEvt); }; return () => { observer.disconnect(); clearWindowEvt(); - cvsEl.removeEventListener("mousedown", onCvsMouseDown); + cvsEl.removeEventListener('mousedown', onCvsMouseDown); }; } @@ -195,11 +195,11 @@ function scaleRect({ }; const clearWindowEvt = (): void => { - window.removeEventListener("mousemove", onMouseMove); - window.removeEventListener("mouseup", clearWindowEvt); + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', clearWindowEvt); }; - window.addEventListener("mousemove", onMouseMove); - window.addEventListener("mouseup", clearWindowEvt); + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', clearWindowEvt); } /** @@ -226,15 +226,15 @@ function stretchScale({ let incW = 0; let incH = 0; let rotateAngle = angle; - if (ctrlKey === "l" || ctrlKey === "r") { + if (ctrlKey === 'l' || ctrlKey === 'r') { incS = deltaX * Math.cos(angle) + deltaY * Math.sin(angle); // l 缩放是反向的 - incW = incS * (ctrlKey === "l" ? -1 : 1); - } else if (ctrlKey === "t" || ctrlKey === "b") { + incW = incS * (ctrlKey === 'l' ? -1 : 1); + } else if (ctrlKey === 't' || ctrlKey === 'b') { // 计算矩形增加的宽度,旋转坐标系让x轴与角度重合,鼠标位置在x轴的投影(x值)即为增加的高度 rotateAngle = angle - Math.PI / 2; incS = deltaX * Math.cos(rotateAngle) + deltaY * Math.sin(rotateAngle); - incH = incS * (ctrlKey === "b" ? -1 : 1); + incH = incS * (ctrlKey === 'b' ? -1 : 1); } return { incW, incH, incS, rotateAngle }; @@ -263,11 +263,11 @@ function fixedRatioScale({ } { // 坐标系旋转角度, lb->rt的对角线的初始角度为负数,所以需要乘以-1 const rotateAngle = - (ctrlKey === "lt" || ctrlKey === "rb" ? 1 : -1) * diagonalAngle + angle; + (ctrlKey === 'lt' || ctrlKey === 'rb' ? 1 : -1) * diagonalAngle + angle; // 旋转坐标系让x轴与对角线重合,鼠标位置在x轴的投影(x值)即为增加的长度 const incS = deltaX * Math.cos(rotateAngle) + deltaY * Math.sin(rotateAngle); // lb lt 缩放值是反向 - const coefficient = ctrlKey === "lt" || ctrlKey === "lb" ? -1 : 1; + const coefficient = ctrlKey === 'lt' || ctrlKey === 'lb' ? -1 : 1; // 等比例缩放,增加宽高等于长度乘以对应的角度函数 // 因为等比例缩放,中心及被拖拽的点,一定在对角线上 const incW = incS * Math.cos(diagonalAngle) * coefficient; @@ -303,7 +303,7 @@ function hitRectCtrls({ ]) ?? []; if (k == null) return false; - if (k === "rotate") { + if (k === 'rotate') { rotateRect(rect, cntMap2Outer(rect.center, cvsRatio, cvsEl)); } else { scaleRect({ @@ -332,11 +332,11 @@ function rotateRect(rect: Rect, outCnt: IPoint): void { rect.angle = angle; }; const clear = (): void => { - window.removeEventListener("mousemove", onMove); - window.removeEventListener("mouseup", clear); + window.removeEventListener('mousemove', onMove); + window.removeEventListener('mouseup', clear); }; - window.addEventListener("mousemove", onMove); - window.addEventListener("mouseup", clear); + window.addEventListener('mousemove', onMove); + window.addEventListener('mouseup', clear); } /** diff --git a/packages/av-canvas/src/sprites/text-sprite.ts b/packages/av-canvas/src/sprites/text-sprite.ts index f9073ae3..d80f8d21 100644 --- a/packages/av-canvas/src/sprites/text-sprite.ts +++ b/packages/av-canvas/src/sprites/text-sprite.ts @@ -1,4 +1,4 @@ -import { renderTxt2Img, BaseSprite } from "@webav/av-cliper"; +import { renderTxt2Img, BaseSprite } from '@webav/av-cliper'; interface IFontStyle { color: string; @@ -12,9 +12,9 @@ export class TextSprite extends BaseSprite { constructor(name: string, content: string, style: Partial = {}) { super(name); const s = { - color: "#ffffff", + color: '#ffffff', size: 100, - family: "sans-serif", + family: 'sans-serif', ...style, }; diff --git a/packages/av-canvas/src/sprites/video-sprite.ts b/packages/av-canvas/src/sprites/video-sprite.ts index 41d4bcfb..ea6fe509 100644 --- a/packages/av-canvas/src/sprites/video-sprite.ts +++ b/packages/av-canvas/src/sprites/video-sprite.ts @@ -1,5 +1,5 @@ -import { createEl } from "../utils"; -import { BaseSprite } from "@webav/av-cliper"; +import { createEl } from '../utils'; +import { BaseSprite } from '@webav/av-cliper'; interface IVideoSpriteOpts { audioCtx?: AudioContext; @@ -63,9 +63,9 @@ export class VideoSprite extends BaseSprite { videoEl: HTMLVideoElement; audioSource: AudioNode | null; }> { - if (!["video/mp4", "video/webm"].includes(videoFile.type)) - throw Error("Unsupport video format"); - const videoEl = createEl("video") as HTMLVideoElement; + if (!['video/mp4', 'video/webm'].includes(videoFile.type)) + throw Error('Unsupport video format'); + const videoEl = createEl('video') as HTMLVideoElement; videoEl.src = URL.createObjectURL(videoFile); await videoEl.play(); @@ -104,7 +104,7 @@ export class VideoSprite extends BaseSprite { async function mediaStream2Video( stream: MediaStream, ): Promise { - const video = document.createElement("video"); + const video = document.createElement('video'); let timer: number; @@ -112,14 +112,14 @@ async function mediaStream2Video( return await new Promise((resolve, reject) => { let failed = false; - video.addEventListener("loadeddata", () => { + video.addEventListener('loadeddata', () => { if (failed) return; clearTimeout(timer); resolve(video); }); timer = window.setTimeout(() => { failed = true; - reject(new Error("video load failed")); + reject(new Error('video load failed')); }, 2000); }); } diff --git a/packages/av-canvas/src/vite-env.d.ts b/packages/av-canvas/src/vite-env.d.ts index 951bda97..e058f21f 100644 --- a/packages/av-canvas/src/vite-env.d.ts +++ b/packages/av-canvas/src/vite-env.d.ts @@ -1 +1 @@ -import "vite/client"; +import 'vite/client'; diff --git a/packages/av-canvas/vite.config.ts b/packages/av-canvas/vite.config.ts index 1665dfae..fe7b5e8c 100644 --- a/packages/av-canvas/vite.config.ts +++ b/packages/av-canvas/vite.config.ts @@ -1,17 +1,17 @@ -import { resolve } from "path"; -import { defineConfig } from "vitest/config"; +import { resolve } from 'path'; +import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - environment: "jsdom", + environment: 'jsdom', coverage: { - provider: "istanbul", // or 'c8' + provider: 'istanbul', // or 'c8' }, }, build: { sourcemap: true, lib: { - entry: resolve(__dirname, "src/index.ts"), - name: "av-canvas", + entry: resolve(__dirname, 'src/index.ts'), + name: 'av-canvas', }, }, }); diff --git a/packages/av-cliper/demo-vite.config.ts b/packages/av-cliper/demo-vite.config.ts index d9bef0b3..79686a21 100644 --- a/packages/av-cliper/demo-vite.config.ts +++ b/packages/av-cliper/demo-vite.config.ts @@ -1,10 +1,10 @@ -import { resolve } from "path"; -import { defineConfig } from "vitest/config"; +import { resolve } from 'path'; +import { defineConfig } from 'vitest/config'; export default defineConfig({ server: { - host: "0.0.0.0", + host: '0.0.0.0', port: 6066, }, - publicDir: resolve(__dirname, "../../doc-site/public"), + publicDir: resolve(__dirname, '../../doc-site/public'), }); diff --git a/packages/av-cliper/demo/chromakey.demo.ts b/packages/av-cliper/demo/chromakey.demo.ts index acacb764..5a434a14 100644 --- a/packages/av-cliper/demo/chromakey.demo.ts +++ b/packages/av-cliper/demo/chromakey.demo.ts @@ -1,13 +1,13 @@ -import { createChromakey } from "../src/chromakey"; +import { createChromakey } from '../src/chromakey'; -const cvs = document.querySelector("#canvas") as HTMLCanvasElement; -const ctx = cvs.getContext("2d", { +const cvs = document.querySelector('#canvas') as HTMLCanvasElement; +const ctx = cvs.getContext('2d', { alpha: true, })!; (async () => { const img = new Image(); - img.src = "./img/green-dog.jpeg"; + img.src = './img/green-dog.jpeg'; await new Promise((resolve) => { img.onload = resolve; }); @@ -16,8 +16,8 @@ const ctx = cvs.getContext("2d", { smoothness: 0.05, spill: 0.05, }); - console.time("cost"); + console.time('cost'); // ctx.drawImage(await chromakey(await createImageBitmap(img)), 0, 0, cvs.width, cvs.height) ctx.drawImage(await chromakey(img), 0, 0, cvs.width, cvs.height); - console.timeEnd("cost"); + console.timeEnd('cost'); })(); diff --git a/packages/av-cliper/demo/concat-media.ts b/packages/av-cliper/demo/concat-media.ts index d037a8c3..f8e39517 100644 --- a/packages/av-cliper/demo/concat-media.ts +++ b/packages/av-cliper/demo/concat-media.ts @@ -1,48 +1,48 @@ -import { AudioClip, ImgClip, MP4Clip, concatAudioClip } from "../src/clips"; -import { Combinator } from "../src/combinator"; -import { Log } from "../src/log"; -import { OffscreenSprite } from "../src/offscreen-sprite"; -import { renderTxt2ImgBitmap } from "../src/dom-utils"; -import { EmbedSubtitlesClip } from "../src/clips/embed-subtitles-clip"; -import { playOutputStream } from "./play-video"; -import { createChromakey, fastConcatMP4 } from "../src"; +import { AudioClip, ImgClip, MP4Clip, concatAudioClip } from '../src/clips'; +import { Combinator } from '../src/combinator'; +import { Log } from '../src/log'; +import { OffscreenSprite } from '../src/offscreen-sprite'; +import { renderTxt2ImgBitmap } from '../src/dom-utils'; +import { EmbedSubtitlesClip } from '../src/clips/embed-subtitles-clip'; +import { playOutputStream } from './play-video'; +import { createChromakey, fastConcatMP4 } from '../src'; // const cvs = document.querySelector('canvas') as HTMLCanvasElement // const ctx = cvs.getContext('2d')! (async () => { if (!(await Combinator.isSupported())) { - alert("Your browser does not support WebCodecs"); + alert('Your browser does not support WebCodecs'); } })(); -const playerContiner = document.querySelector("#player-continer")!; +const playerContiner = document.querySelector('#player-continer')!; -document.querySelector("#mp4-img")?.addEventListener("click", () => { +document.querySelector('#mp4-img')?.addEventListener('click', () => { (async () => { - const resList = ["./video/webav1.mp4", "./img/bunny.png"]; + const resList = ['./video/webav1.mp4', './img/bunny.png']; const { loadStream } = playOutputStream(resList, playerContiner); const spr1 = new OffscreenSprite( - "spr1", + 'spr1', new MP4Clip((await fetch(resList[0])).body!), ); const spr2 = new OffscreenSprite( - "spr2", + 'spr2', new ImgClip( await renderTxt2ImgBitmap( - "水印", + '水印', `font-size:40px; color: white; text-shadow: 2px 2px 6px red;`, ), ), ); spr2.setAnimation( { - "0%": { x: 0, y: 0 }, - "25%": { x: 1200, y: 680 }, - "50%": { x: 1200, y: 0 }, - "75%": { x: 0, y: 680 }, - "100%": { x: 0, y: 0 }, + '0%': { x: 0, y: 0 }, + '25%': { x: 1200, y: 680 }, + '50%': { x: 1200, y: 0 }, + '75%': { x: 0, y: 680 }, + '100%': { x: 0, y: 0 }, }, { duration: 4, iterCount: 1 }, ); @@ -50,7 +50,7 @@ document.querySelector("#mp4-img")?.addEventListener("click", () => { spr2.opacity = 0.5; const spr3 = new OffscreenSprite( - "spr3", + 'spr3', new ImgClip( await createImageBitmap(await (await fetch(resList[1])).blob()), ), @@ -68,8 +68,8 @@ document.querySelector("#mp4-img")?.addEventListener("click", () => { const com = new Combinator({ width: 1280, height: 720, - videoCodec: "avc1.42E032", - bgColor: "white", + videoCodec: 'avc1.42E032', + bgColor: 'white', }); await com.add(spr1, { main: true }); @@ -80,18 +80,18 @@ document.querySelector("#mp4-img")?.addEventListener("click", () => { })().catch(Log.error); }); -document.querySelector("#mp4-mp3")?.addEventListener("click", () => { +document.querySelector('#mp4-mp3')?.addEventListener('click', () => { (async () => { - const resList = ["./video/webav1.mp4", "./audio/44.1kHz-2chan.mp3"]; + const resList = ['./video/webav1.mp4', './audio/44.1kHz-2chan.mp3']; const { loadStream } = playOutputStream(resList, playerContiner); // const resp1 = await fetch('./video/pri-bunny_avc_frag.mp4') const resp1 = await fetch(resList[0]); - const spr1 = new OffscreenSprite("spr1", new MP4Clip(resp1.body!)); + const spr1 = new OffscreenSprite('spr1', new MP4Clip(resp1.body!)); const resp2 = await fetch(resList[1]); const spr2 = new OffscreenSprite( - "spr2", + 'spr2', new AudioClip(resp2.body!, { // volume: 2, loop: true, @@ -108,18 +108,18 @@ document.querySelector("#mp4-mp3")?.addEventListener("click", () => { })().catch(Log.error); }); -document.querySelector("#mix-audio")?.addEventListener("click", () => { +document.querySelector('#mix-audio')?.addEventListener('click', () => { (async () => { - const resList = ["./audio/44.1kHz-2chan.m4a", "./audio/16kHz-1chan.mp3"]; + const resList = ['./audio/44.1kHz-2chan.m4a', './audio/16kHz-1chan.mp3']; const { loadStream } = playOutputStream(resList, playerContiner); const resp1 = await fetch(resList[0]); const resp2 = await fetch(resList[1]); const spr1 = new OffscreenSprite( - "1", + '1', new AudioClip(resp1.body!, { volume: 0.5 }), ); - const spr2 = new OffscreenSprite("2", new AudioClip(resp2.body!)); + const spr2 = new OffscreenSprite('2', new AudioClip(resp2.body!)); const com = new Combinator({ width: 1280, height: 720 }); await com.add(spr1, { offset: 0, duration: 5 }); @@ -129,9 +129,9 @@ document.querySelector("#mix-audio")?.addEventListener("click", () => { })().catch(Log.error); }); -document.querySelector("#concat-audio")?.addEventListener("click", () => { +document.querySelector('#concat-audio')?.addEventListener('click', () => { (async () => { - const resList = ["./audio/16kHz-1chan.mp3", "./audio/44.1kHz-2chan.m4a"]; + const resList = ['./audio/16kHz-1chan.mp3', './audio/44.1kHz-2chan.m4a']; const { loadStream } = playOutputStream(resList, playerContiner); const clip = await concatAudioClip( @@ -139,7 +139,7 @@ document.querySelector("#concat-audio")?.addEventListener("click", () => { resList.map(async (url) => new AudioClip((await fetch(url)).body!)), ), ); - const spr1 = new OffscreenSprite("1", clip); + const spr1 = new OffscreenSprite('1', clip); const com = new Combinator({ width: 1280, height: 720 }); await com.add(spr1, { offset: 0, duration: 30 }); @@ -148,18 +148,18 @@ document.querySelector("#concat-audio")?.addEventListener("click", () => { })().catch(Log.error); }); -document.querySelector("#gif-m4a")?.addEventListener("click", () => { +document.querySelector('#gif-m4a')?.addEventListener('click', () => { (async () => { - const resList = ["./img/animated.gif", "./audio/44.1kHz-2chan.m4a"]; + const resList = ['./img/animated.gif', './audio/44.1kHz-2chan.m4a']; const { loadStream } = playOutputStream(resList, playerContiner); const resp1 = await fetch(resList[0]); const spr1 = new OffscreenSprite( - "s1", - new ImgClip({ type: "image/gif", stream: resp1.body! }), + 's1', + new ImgClip({ type: 'image/gif', stream: resp1.body! }), ); const resp2 = await fetch(resList[1]); - const spr2 = new OffscreenSprite("s2", new AudioClip(resp2.body!)); + const spr2 = new OffscreenSprite('s2', new AudioClip(resp2.body!)); const com = new Combinator({ width: 1280, height: 720 }); await com.add(spr1, { duration: 10, offset: 0 }); await com.add(spr2, { duration: 10, offset: 0 }); @@ -168,30 +168,30 @@ document.querySelector("#gif-m4a")?.addEventListener("click", () => { })(); }); -document.querySelector("#mp4-srt")?.addEventListener("click", () => { +document.querySelector('#mp4-srt')?.addEventListener('click', () => { (async () => { - const resList = ["./video/webav1.mp4", "./subtitles/test-sample.srt"]; + const resList = ['./video/webav1.mp4', './subtitles/test-sample.srt']; const { loadStream } = playOutputStream(resList, playerContiner); const resp1 = await fetch(resList[0]); - const spr1 = new OffscreenSprite("s1", new MP4Clip(resp1.body!)); + const spr1 = new OffscreenSprite('s1', new MP4Clip(resp1.body!)); const resp2 = await fetch(resList[1]); const spr2 = new OffscreenSprite( - "s2", + 's2', new EmbedSubtitlesClip(await resp2.text(), { videoWidth: 1280, videoHeight: 720, fontSize: 44, - fontFamily: "Noto Sans SC", - strokeStyle: "#000", + fontFamily: 'Noto Sans SC', + strokeStyle: '#000', lineWidth: 20, - lineJoin: "round", - lineCap: "round", + lineJoin: 'round', + lineCap: 'round', textShadow: { offsetX: 2, offsetY: 2, blur: 4, - color: "rgba(0,0,0,0.25)", + color: 'rgba(0,0,0,0.25)', }, }), ); @@ -203,9 +203,9 @@ document.querySelector("#mp4-srt")?.addEventListener("click", () => { })(); }); -document.querySelector("#mp4-chromakey")?.addEventListener("click", () => { +document.querySelector('#mp4-chromakey')?.addEventListener('click', () => { (async () => { - const resList = ["./video/chromakey-test.mp4", "./img/bunny.png"]; + const resList = ['./video/chromakey-test.mp4', './img/bunny.png']; const { loadStream } = playOutputStream(resList, playerContiner); const width = 1280; @@ -217,7 +217,7 @@ document.querySelector("#mp4-chromakey")?.addEventListener("click", () => { spill: 0.1, }); const originSpr = new OffscreenSprite( - "originSpr", + 'originSpr', new MP4Clip((await fetch(resList[0])).body!), ); await originSpr.ready; @@ -234,14 +234,14 @@ document.querySelector("#mp4-chromakey")?.addEventListener("click", () => { }; }; - const targetSpr = new OffscreenSprite("targetSpr", targetClip); + const targetSpr = new OffscreenSprite('targetSpr', targetClip); await targetSpr.ready; targetSpr.zIndex = 1; targetSpr.rect.x = originSpr.rect.x + targetSpr.rect.w + 100; targetSpr.rect.y = (height - targetSpr.rect.h) / 2; const bgImgSpr = new OffscreenSprite( - "bgImgSpr", + 'bgImgSpr', new ImgClip( await createImageBitmap(await (await fetch(resList[1])).blob()), ), @@ -250,7 +250,7 @@ document.querySelector("#mp4-chromakey")?.addEventListener("click", () => { const com = new Combinator({ width, height, - bgColor: "white", + bgColor: 'white', }); await com.add(originSpr, { main: true }); @@ -261,9 +261,9 @@ document.querySelector("#mp4-chromakey")?.addEventListener("click", () => { })().catch(Log.error); }); -document.querySelector("#complex")?.addEventListener("click", () => { +document.querySelector('#complex')?.addEventListener('click', () => { (async () => { - const mp4List = ["./video/123.mp4", "./video/223.mp4", "./video/323.mp4"]; + const mp4List = ['./video/123.mp4', './video/223.mp4', './video/323.mp4']; const width = 1280; const height = 720; @@ -290,14 +290,14 @@ document.querySelector("#complex")?.addEventListener("click", () => { }; return clip; }) - .map((clip) => new OffscreenSprite("spr", clip)) + .map((clip) => new OffscreenSprite('spr', clip)) .map(async (spr, idx) => { const com = new Combinator({ width, height }); const imgSpr = new OffscreenSprite( - "spr3", + 'spr3', new ImgClip( await createImageBitmap( - await (await fetch("./img/bunny.png")).blob(), + await (await fetch('./img/bunny.png')).blob(), ), ), ); diff --git a/packages/av-cliper/demo/decode-media.ts b/packages/av-cliper/demo/decode-media.ts index 92c9d7d1..8ef59891 100644 --- a/packages/av-cliper/demo/decode-media.ts +++ b/packages/av-cliper/demo/decode-media.ts @@ -1,35 +1,35 @@ -import { Combinator } from "../src"; -import { decodeImg, sleep } from "../src/av-utils"; -import { createChromakey } from "../src/chromakey"; -import { AudioClip, DEFAULT_AUDIO_CONF, MP4Clip } from "../src/clips"; -import { EmbedSubtitlesClip } from "../src/clips/embed-subtitles-clip"; -import { Log } from "../src/log"; +import { Combinator } from '../src'; +import { decodeImg, sleep } from '../src/av-utils'; +import { createChromakey } from '../src/chromakey'; +import { AudioClip, DEFAULT_AUDIO_CONF, MP4Clip } from '../src/clips'; +import { EmbedSubtitlesClip } from '../src/clips/embed-subtitles-clip'; +import { Log } from '../src/log'; -import Worker from "./decode-video-worker?worker&inline"; +import Worker from './decode-video-worker?worker&inline'; new Worker(); (async () => { if (!(await Combinator.isSupported())) { - alert("Your browser does not support WebCodecs"); + alert('Your browser does not support WebCodecs'); } })(); -const cvs = document.querySelector("canvas") as HTMLCanvasElement; -const ctx = cvs.getContext("2d")!; +const cvs = document.querySelector('canvas') as HTMLCanvasElement; +const ctx = cvs.getContext('2d')!; const imgs = { - "image/avif": "./img/animated.avif", - "image/webp": "./img/animated.webp", - "image/png": "./img/animated.png", - "image/gif": "./img/animated.gif", + 'image/avif': './img/animated.avif', + 'image/webp': './img/animated.webp', + 'image/png': './img/animated.png', + 'image/gif': './img/animated.gif', }; let stopImg = () => {}; -document.querySelector("#decode-img")?.addEventListener("click", () => { +document.querySelector('#decode-img')?.addEventListener('click', () => { (async () => { stopImg(); const imgType = ( - document.querySelector("input[name=img-type]:checked") as HTMLInputElement + document.querySelector('input[name=img-type]:checked') as HTMLInputElement ).value; // @ts-expect-error @@ -56,18 +56,18 @@ document.querySelector("#decode-img")?.addEventListener("click", () => { }); const audios = { - "44.1kHz-2chan.m4a": "./audio/44.1kHz-2chan.m4a", - "44.1kHz-2chan.mp3": "./audio/44.1kHz-2chan.mp3", - "16kHz-1chan.mp3": "./audio/16kHz-1chan.mp3", + '44.1kHz-2chan.m4a': './audio/44.1kHz-2chan.m4a', + '44.1kHz-2chan.mp3': './audio/44.1kHz-2chan.mp3', + '16kHz-1chan.mp3': './audio/16kHz-1chan.mp3', }; let stopAudio = () => {}; -document.querySelector("#decode-audio")?.addEventListener("click", () => { +document.querySelector('#decode-audio')?.addEventListener('click', () => { (async () => { stopAudio(); const audioType = ( document.querySelector( - "input[name=audio-type]:checked", + 'input[name=audio-type]:checked', ) as HTMLInputElement ).value; // @ts-expect-error @@ -81,8 +81,8 @@ document.querySelector("#decode-audio")?.addEventListener("click", () => { async function play() { time += 100000; const { audio, state } = await clip.tick(time); - if (state === "done") { - console.log("--- ended"); + if (state === 'done') { + console.log('--- ended'); return; } const len = audio[0].length; @@ -113,18 +113,18 @@ document.querySelector("#decode-audio")?.addEventListener("click", () => { }); const videos = { - "bunny.mp4": "./video/bunny-avc.mp4", - "bear.mp4": "./video/bear-vp9.mp4", + 'bunny.mp4': './video/bunny-avc.mp4', + 'bear.mp4': './video/bear-vp9.mp4', }; -document.querySelector("#decode-video")?.addEventListener("click", () => { +document.querySelector('#decode-video')?.addEventListener('click', () => { (async () => { const videoType = ( document.querySelector( - "input[name=video-type]:checked", + 'input[name=video-type]:checked', ) as HTMLInputElement ).value; const speed = document.querySelector( - "input[name=playrate]:checked", + 'input[name=playrate]:checked', ) as HTMLInputElement; // @ts-expect-error @@ -132,7 +132,7 @@ document.querySelector("#decode-video")?.addEventListener("click", () => { const clip = new MP4Clip(resp1.body!); await clip.ready; - if (speed.value === "fastest") { + if (speed.value === 'fastest') { fastestDecode(); } else { timesSpeedDecode(Number(speed.value)); @@ -142,8 +142,8 @@ document.querySelector("#decode-video")?.addEventListener("click", () => { let time = 0; while (true) { const { state, video } = await clip.tick(time); - if (state === "done") break; - if (video != null && state === "success") { + if (state === 'done') break; + if (video != null && state === 'success') { ctx.clearRect(0, 0, cvs.width, cvs.height); ctx.drawImage(video, 0, 0, video.codedWidth, video.codedHeight); video.close(); @@ -160,12 +160,12 @@ document.querySelector("#decode-video")?.addEventListener("click", () => { const { state, video } = await clip.tick( Math.round((performance.now() - startTime) * 1000) * times, ); - if (state === "done") { + if (state === 'done') { clearInterval(timer); clip.destroy(); return; } - if (video != null && state === "success") { + if (video != null && state === 'success') { ctx.clearRect(0, 0, cvs.width, cvs.height); ctx.drawImage(video, 0, 0, video.codedWidth, video.codedHeight); video.close(); @@ -176,14 +176,14 @@ document.querySelector("#decode-video")?.addEventListener("click", () => { }); const subtitles = { - "test-sample.srt": "./subtitles/test-sample.srt", + 'test-sample.srt': './subtitles/test-sample.srt', }; -document.querySelector("#decode-subtitles")?.addEventListener("click", () => { +document.querySelector('#decode-subtitles')?.addEventListener('click', () => { (async () => { stopImg(); const subtitlesType = ( document.querySelector( - "input[name=subtitles-type]:checked", + 'input[name=subtitles-type]:checked', ) as HTMLInputElement ).value; @@ -195,27 +195,27 @@ document.querySelector("#decode-subtitles")?.addEventListener("click", () => { videoHeight: 720, fontSize: 40, // textBgColor: '#000000', - color: "yellow", + color: 'yellow', }); let time = 0; while (time < 20 * 1e6) { const { state, video } = await es.tick(time); - if (state === "done") break; + if (state === 'done') break; ctx.clearRect(0, 0, cvs.width, cvs.height); ctx.drawImage(video!, 0, 0); video?.close(); time += 33000; await sleep(10); } - console.log("decode subtitles done"); + console.log('decode subtitles done'); es.destroy(); })(); }); -document.querySelector("#chromakey")?.addEventListener("click", () => { +document.querySelector('#chromakey')?.addEventListener('click', () => { (async () => { - const clip = new MP4Clip((await fetch("./video/chromakey-test.mp4")).body!); + const clip = new MP4Clip((await fetch('./video/chromakey-test.mp4')).body!); const chromakey = createChromakey({ similarity: 0.4, smoothness: 0.1, @@ -231,11 +231,11 @@ document.querySelector("#chromakey")?.addEventListener("click", () => { let time = 0; const timerId = setInterval(async () => { const { state, video } = await clip.tick(time); - if (state === "done") { + if (state === 'done') { clearInterval(timerId); clip.destroy(); } - if (video != null && state === "success") { + if (video != null && state === 'success') { ctx.clearRect(0, 0, cvs.width, cvs.height); ctx.drawImage(video, 0, 0, video.codedWidth, video.codedHeight); video.close(); diff --git a/packages/av-cliper/demo/decode-video-worker.ts b/packages/av-cliper/demo/decode-video-worker.ts index ec516169..da86225a 100644 --- a/packages/av-cliper/demo/decode-video-worker.ts +++ b/packages/av-cliper/demo/decode-video-worker.ts @@ -1,14 +1,14 @@ -import { MP4Clip } from "../src/clips"; +import { MP4Clip } from '../src/clips'; (async () => { - const resp1 = await fetch("./video/123.mp4"); + const resp1 = await fetch('./video/123.mp4'); const clip = new MP4Clip(resp1.body!); await clip.ready; let time = 0; while (true) { const { state, video, audio } = await clip.tick(time); - console.log("worker decode", { time, video, audio, state }); - if (state === "done") break; - if (video != null && state === "success") { + console.log('worker decode', { time, video, audio, state }); + if (state === 'done') break; + if (video != null && state === 'success') { video.close(); } time += 33000; diff --git a/packages/av-cliper/demo/fast-concat-mp4.ts b/packages/av-cliper/demo/fast-concat-mp4.ts index e8177019..bddb045d 100644 --- a/packages/av-cliper/demo/fast-concat-mp4.ts +++ b/packages/av-cliper/demo/fast-concat-mp4.ts @@ -1,10 +1,10 @@ -import { Log } from "../src/log"; -import { fastConcatMP4, mixinMP4AndAudio } from "../src/mp4-utils"; -import { playOutputStream } from "./play-video"; +import { Log } from '../src/log'; +import { fastConcatMP4, mixinMP4AndAudio } from '../src/mp4-utils'; +import { playOutputStream } from './play-video'; -document.querySelector("#fast-concat-mp4")?.addEventListener("click", () => { +document.querySelector('#fast-concat-mp4')?.addEventListener('click', () => { (async () => { - const resList = ["./video/webav1.mp4", "./video/webav2.mp4"]; + const resList = ['./video/webav1.mp4', './video/webav2.mp4']; const stream = await fastConcatMP4( await Promise.all(resList.map(async (url) => (await fetch(url)).body!)), ); @@ -13,9 +13,9 @@ document.querySelector("#fast-concat-mp4")?.addEventListener("click", () => { })().catch(Log.error); }); -document.querySelector("#mixin-mp4-audio")?.addEventListener("click", () => { +document.querySelector('#mixin-mp4-audio')?.addEventListener('click', () => { (async () => { - const resList = ["./video/webav1.mp4", "./audio/44.1kHz-2chan.mp3"]; + const resList = ['./video/webav1.mp4', './audio/44.1kHz-2chan.mp3']; const stream = mixinMP4AndAudio((await fetch(resList[0])).body!, { stream: (await fetch(resList[1])).body!, volume: 1, @@ -26,12 +26,12 @@ document.querySelector("#mixin-mp4-audio")?.addEventListener("click", () => { })().catch(Log.error); }); -document.querySelector("#concat-and-mixin")?.addEventListener("click", () => { +document.querySelector('#concat-and-mixin')?.addEventListener('click', () => { (async () => { const resList = [ - "./video/webav1.mp4", - "./video/webav2.mp4", - "./audio/44.1kHz-2chan.mp3", + './video/webav1.mp4', + './video/webav2.mp4', + './audio/44.1kHz-2chan.mp3', ]; const mp4Stream = await fastConcatMP4([ (await fetch(resList[0])).body!, diff --git a/packages/av-cliper/demo/mp4-previewer.demo.ts b/packages/av-cliper/demo/mp4-previewer.demo.ts index d3d6cdf2..065a89a1 100644 --- a/packages/av-cliper/demo/mp4-previewer.demo.ts +++ b/packages/av-cliper/demo/mp4-previewer.demo.ts @@ -1,12 +1,12 @@ -import { MP4Previewer } from "../src/mp4-utils/mp4-previewer"; +import { MP4Previewer } from '../src/mp4-utils/mp4-previewer'; -const previewer = new MP4Previewer((await fetch("./video/webav1.mp4")).body!); -const imgEl = document.querySelector("#img") as HTMLImageElement; +const previewer = new MP4Previewer((await fetch('./video/webav1.mp4')).body!); +const imgEl = document.querySelector('#img') as HTMLImageElement; for (let i = 0; i < 10; i += 1) { const t = performance.now(); const img = await previewer.getImage(i); - console.log("cost:", performance.now() - t, img); + console.log('cost:', performance.now() - t, img); if (img == null) break; imgEl.src = img; } diff --git a/packages/av-cliper/demo/play-video.ts b/packages/av-cliper/demo/play-video.ts index 4cfff66d..a53f7dd6 100644 --- a/packages/av-cliper/demo/play-video.ts +++ b/packages/av-cliper/demo/play-video.ts @@ -1,22 +1,22 @@ -import { Combinator } from "../src"; +import { Combinator } from '../src'; export function playOutputStream(resourceList: string[], attachEl: Element) { - const container = document.createElement("div"); + const container = document.createElement('div'); attachEl.appendChild(container); - const resourceEl = document.createElement("div"); + const resourceEl = document.createElement('div'); resourceEl.innerHTML = `Resource:
` + resourceList .map((str) => `${str}`) - .join("
"); + .join('
'); container.appendChild(resourceEl); - const stateEl = document.createElement("div"); - stateEl.textContent = "loading..."; + const stateEl = document.createElement('div'); + stateEl.textContent = 'loading...'; container.appendChild(stateEl); - const videoEl = document.createElement("video"); + const videoEl = document.createElement('video'); videoEl.controls = true; videoEl.autoplay = true; videoEl.style.cssText = ` @@ -25,12 +25,12 @@ export function playOutputStream(resourceList: string[], attachEl: Element) { display: block; `; - const btnContiner = document.createElement("div"); + const btnContiner = document.createElement('div'); container.appendChild(btnContiner); - const closeEl = document.createElement("button"); - closeEl.textContent = "close"; - closeEl.style.marginRight = "16px"; + const closeEl = document.createElement('button'); + closeEl.textContent = 'close'; + closeEl.style.marginRight = '16px'; btnContiner.appendChild(closeEl); container.appendChild(videoEl); @@ -46,8 +46,8 @@ export function playOutputStream(resourceList: string[], attachEl: Element) { URL.revokeObjectURL(videoEl.src); }; - com?.on("OutputProgress", (v) => { - console.log("----- progress:", v); + com?.on('OutputProgress', (v) => { + console.log('----- progress:', v); stateEl.textContent = `progress: ${Math.round(v * 100)}%`; }); @@ -65,14 +65,14 @@ export function playOutputStream(resourceList: string[], attachEl: Element) { } function createDownloadBtn(url: string) { - const downloadEl = document.createElement("button"); - downloadEl.textContent = "download"; + const downloadEl = document.createElement('button'); + downloadEl.textContent = 'download'; downloadEl.onclick = () => { - const aEl = document.createElement("a"); + const aEl = document.createElement('a'); document.body.appendChild(aEl); - aEl.setAttribute("href", url); - aEl.setAttribute("download", `WebAV-export-${Date.now()}.mp4`); - aEl.setAttribute("target", "_self"); + aEl.setAttribute('href', url); + aEl.setAttribute('download', `WebAV-export-${Date.now()}.mp4`); + aEl.setAttribute('target', '_self'); aEl.click(); }; return downloadEl; diff --git a/packages/av-cliper/src/__tests__/av-utils.test.ts b/packages/av-cliper/src/__tests__/av-utils.test.ts index d7095143..714f2bd0 100644 --- a/packages/av-cliper/src/__tests__/av-utils.test.ts +++ b/packages/av-cliper/src/__tests__/av-utils.test.ts @@ -1,14 +1,14 @@ -import { test, expect } from "vitest"; -import "./mock"; -import { concatFloat32Array, concatPCMFragments, mixinPCM } from "../av-utils"; +import { test, expect } from 'vitest'; +import './mock'; +import { concatFloat32Array, concatPCMFragments, mixinPCM } from '../av-utils'; -test("concatArrayBuffer", () => { +test('concatArrayBuffer', () => { expect( concatFloat32Array([new Float32Array([1]), new Float32Array([2])]), ).toEqual(new Float32Array([1, 2])); }); -test("mixinPCM", () => { +test('mixinPCM', () => { const wav1 = new Float32Array([1, 1, 1]); const wav2 = new Float32Array([2, 2, 2, 2, 2]); @@ -23,7 +23,7 @@ test("mixinPCM", () => { ).toEqual(new Float32Array([3, 3, 3, 2, 2, 3, 3, 3, 2, 2])); }); -test("mixinPCM empty track", () => { +test('mixinPCM empty track', () => { expect( mixinPCM([ [new Float32Array([2, 2, 2, 2, 2]), new Float32Array([0, 0, 0, 0, 0])], @@ -32,7 +32,7 @@ test("mixinPCM empty track", () => { ).toEqual(new Float32Array([2, 2, 2, 2, 2, 0, 0, 0, 0, 0])); }); -test("concatFragmentPCM", () => { +test('concatFragmentPCM', () => { const chan0 = new Float32Array([0, 0, 0]); const chan1 = new Float32Array([1, 1, 1]); expect( diff --git a/packages/av-cliper/src/__tests__/base-sprite.test.ts b/packages/av-cliper/src/__tests__/base-sprite.test.ts index 975a4629..450e883c 100644 --- a/packages/av-cliper/src/__tests__/base-sprite.test.ts +++ b/packages/av-cliper/src/__tests__/base-sprite.test.ts @@ -1,7 +1,7 @@ -import { describe, expect, test } from "vitest"; -import { TAnimationKeyFrame, linearTimeFn } from "../base-sprite"; +import { describe, expect, test } from 'vitest'; +import { TAnimationKeyFrame, linearTimeFn } from '../base-sprite'; -describe("Animation", () => { +describe('Animation', () => { const keyFrames: TAnimationKeyFrame = [ [0, { angle: 0, x: 0, opacity: 1 }], [0.2, { angle: Math.PI / 2, x: 50, opacity: 0.5 }], @@ -13,17 +13,17 @@ describe("Animation", () => { iterCount: Infinity, }; - test("linearTimeFn 10%", () => { + test('linearTimeFn 10%', () => { const rs = linearTimeFn(1, keyFrames, opts); expect(rs).toEqual({ angle: Math.PI / 4, x: 25, opacity: 0.75 }); }); - test("linearTimeFn 20%", () => { + test('linearTimeFn 20%', () => { const rs = linearTimeFn(2, keyFrames, opts); expect(rs).toEqual({ angle: Math.PI / 2, x: 50, opacity: 0.5 }); }); - test("linearTimeFn 100%", () => { + test('linearTimeFn 100%', () => { const rs = linearTimeFn(10, keyFrames, opts); expect(rs).toEqual({ angle: Math.PI, x: 100, opacity: 0 }); }); diff --git a/packages/av-cliper/src/__tests__/event-tool.test.ts b/packages/av-cliper/src/__tests__/event-tool.test.ts index 95d9fe49..cd51aee4 100644 --- a/packages/av-cliper/src/__tests__/event-tool.test.ts +++ b/packages/av-cliper/src/__tests__/event-tool.test.ts @@ -1,7 +1,7 @@ -import { expect, test, vi } from "vitest"; -import { EventTool } from "../event-tool"; +import { expect, test, vi } from 'vitest'; +import { EventTool } from '../event-tool'; -test("event-tool", () => { +test('event-tool', () => { const evtTool = new EventTool<{ a: (str: string) => void; b: () => void; @@ -9,19 +9,19 @@ test("event-tool", () => { const onA = vi.fn(); const onB = vi.fn(); - evtTool.on("a", onA); - const offB = evtTool.on("b", onB); + evtTool.on('a', onA); + const offB = evtTool.on('b', onB); offB(); - evtTool.emit("a", "test"); - evtTool.emit("b"); + evtTool.emit('a', 'test'); + evtTool.emit('b'); - expect(onA).toBeCalledWith("test"); + expect(onA).toBeCalledWith('test'); expect(onB).not.toBeCalled(); onA.mockClear(); evtTool.destroy(); - evtTool.emit("a", "test"); + evtTool.emit('a', 'test'); expect(onA).not.toBeCalled(); }); diff --git a/packages/av-cliper/src/__tests__/log.test.ts b/packages/av-cliper/src/__tests__/log.test.ts index 8639cb7e..79add924 100644 --- a/packages/av-cliper/src/__tests__/log.test.ts +++ b/packages/av-cliper/src/__tests__/log.test.ts @@ -1,22 +1,22 @@ -import { vi, expect, test } from "vitest"; -import { Log } from "../log"; +import { vi, expect, test } from 'vitest'; +import { Log } from '../log'; -const errorSpy = vi.spyOn(console, "error"); -const warnSpy = vi.spyOn(console, "warn"); -const infoSpy = vi.spyOn(console, "info"); -const debugSpy = vi.spyOn(console, "debug"); -test("log threshold", () => { +const errorSpy = vi.spyOn(console, 'error'); +const warnSpy = vi.spyOn(console, 'warn'); +const infoSpy = vi.spyOn(console, 'info'); +const debugSpy = vi.spyOn(console, 'debug'); +test('log threshold', () => { Log.setLogLevel(Log.debug); - Log.debug("log test"); - expect(debugSpy).toHaveBeenCalledWith("log test"); + Log.debug('log test'); + expect(debugSpy).toHaveBeenCalledWith('log test'); debugSpy.mockReset(); Log.setLogLevel(Log.error); - Log.debug("log test"); - Log.info("log test"); - Log.warn("log test"); - Log.error("log test"); - expect(errorSpy).toHaveBeenCalledWith("log test"); + Log.debug('log test'); + Log.info('log test'); + Log.warn('log test'); + Log.error('log test'); + expect(errorSpy).toHaveBeenCalledWith('log test'); expect(warnSpy).not.toHaveBeenCalled(); expect(infoSpy).not.toHaveBeenCalled(); expect(debugSpy).not.toHaveBeenCalled(); diff --git a/packages/av-cliper/src/__tests__/mock.ts b/packages/av-cliper/src/__tests__/mock.ts index aa47dd51..68888c10 100644 --- a/packages/av-cliper/src/__tests__/mock.ts +++ b/packages/av-cliper/src/__tests__/mock.ts @@ -1,4 +1,4 @@ -import { vi } from "vitest"; +import { vi } from 'vitest'; export const MediaStreamMock = { getTracks: vi.fn().mockReturnValue([]), @@ -74,21 +74,21 @@ Object.assign(global, { }), }); -vi.spyOn(HTMLVideoElement.prototype, "addEventListener").mockImplementation( +vi.spyOn(HTMLVideoElement.prototype, 'addEventListener').mockImplementation( (_, handler: any) => { handler(); }, ); -vi.spyOn(HTMLVideoElement.prototype, "play").mockImplementation(async () => { +vi.spyOn(HTMLVideoElement.prototype, 'play').mockImplementation(async () => { return await Promise.resolve(); }); -vi.spyOn(HTMLAudioElement.prototype, "play").mockImplementation(async () => { +vi.spyOn(HTMLAudioElement.prototype, 'play').mockImplementation(async () => { return await Promise.resolve(); }); -vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(() => { +vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockImplementation(() => { return { drawImage: vi.fn(), fillRect: vi.fn(), @@ -100,10 +100,10 @@ vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(() => { export const CvsElementMock = { clientWidth: vi - .spyOn(HTMLCanvasElement.prototype, "clientWidth", "get") + .spyOn(HTMLCanvasElement.prototype, 'clientWidth', 'get') .mockImplementation(() => 0), clientHeight: vi - .spyOn(HTMLCanvasElement.prototype, "clientHeight", "get") + .spyOn(HTMLCanvasElement.prototype, 'clientHeight', 'get') .mockImplementation(() => 0), }; @@ -114,7 +114,7 @@ Object.assign(HTMLCanvasElement.prototype, { export const getBoundingClientRectMock = vi.spyOn( HTMLElement.prototype, - "getBoundingClientRect", + 'getBoundingClientRect', ); export const createObjectURLMock = (URL.createObjectURL = vi.fn()); @@ -133,8 +133,8 @@ export function crtMSEvt4Offset( offsetY: number, ): MouseEvent { const evt = new MouseEvent(evtName); - vi.spyOn(evt, "offsetX", "get").mockImplementation(() => offsetX); - vi.spyOn(evt, "offsetY", "get").mockImplementation(() => offsetY); + vi.spyOn(evt, 'offsetX', 'get').mockImplementation(() => offsetX); + vi.spyOn(evt, 'offsetY', 'get').mockImplementation(() => offsetY); return evt; } @@ -173,7 +173,7 @@ export const OffscreenCanvasRenderingContext2DMock = { clearRect: vi.fn(), measureText: vi.fn().mockReturnValue({}), strokeText: vi.fn(), - fillStyle: "", + fillStyle: '', }; Object.assign(global, { diff --git a/packages/av-cliper/src/__tests__/rect.test.ts b/packages/av-cliper/src/__tests__/rect.test.ts index 9f9abf59..e88ed529 100644 --- a/packages/av-cliper/src/__tests__/rect.test.ts +++ b/packages/av-cliper/src/__tests__/rect.test.ts @@ -1,23 +1,23 @@ -import { describe, expect, test } from "vitest"; -import { Rect } from "../rect"; +import { describe, expect, test } from 'vitest'; +import { Rect } from '../rect'; -describe("Rect", () => { - test("center", () => { +describe('Rect', () => { + test('center', () => { const rect = new Rect(0, 0, 100, 100); expect(rect.center).toEqual({ x: 50, y: 50 }); }); - test("ctrls", () => { + test('ctrls', () => { const rect = new Rect(0, 0, 100, 100); expect(rect.ctrls).toMatchSnapshot(); }); - test("clone", () => { + test('clone', () => { const { x, y, w, h } = new Rect(0, 0, 100, 100).clone(); expect([x, y, w, h]).toEqual([0, 0, 100, 100]); }); - test("checkHit", () => { + test('checkHit', () => { const rect = new Rect(100, 100, 100, 100); rect.x = 100; rect.y = 100; diff --git a/packages/av-cliper/src/av-utils.ts b/packages/av-cliper/src/av-utils.ts index ddda6683..73790e70 100644 --- a/packages/av-cliper/src/av-utils.ts +++ b/packages/av-cliper/src/av-utils.ts @@ -1,8 +1,8 @@ // 能同时在 worker 和主线程中运行的工具函数 -import { Log } from "./log"; -import { workerTimer } from "./worker-timer"; -import { resample } from "wave-resampler"; +import { Log } from './log'; +import { workerTimer } from './worker-timer'; +import { resample } from 'wave-resampler'; /** * 合并(串联)多个 Float32Array,通常用于合并 PCM 数据 @@ -156,7 +156,7 @@ export async function audioResample( return pcmData.map( (p) => new Float32Array( - resample(p, curRate, target.rate, { method: "sinc", LPF: false }), + resample(p, curRate, target.rate, { method: 'sinc', LPF: false }), ), ); } diff --git a/packages/av-cliper/src/base-sprite.ts b/packages/av-cliper/src/base-sprite.ts index b2f110d3..c1a869e8 100644 --- a/packages/av-cliper/src/base-sprite.ts +++ b/packages/av-cliper/src/base-sprite.ts @@ -1,4 +1,4 @@ -import { IRectBaseProps, Rect } from "./rect"; +import { IRectBaseProps, Rect } from './rect'; interface IAnimationOpts { duration: number; @@ -11,7 +11,7 @@ type TAnimateProps = IRectBaseProps & { opacity: number }; export type TAnimationKeyFrame = Array<[number, Partial]>; type TKeyFrameOpts = Partial< - Record<`${number}%` | "from" | "to", Partial> + Record<`${number}%` | 'from' | 'to', Partial> >; export abstract class BaseSprite { @@ -23,7 +23,7 @@ export abstract class BaseSprite { opacity = 1; - flip: "horizontal" | "vertical" | null = null; + flip: 'horizontal' | 'vertical' | null = null; #animatKeyFrame: TAnimationKeyFrame | null = null; @@ -43,11 +43,11 @@ export abstract class BaseSprite { } = this; ctx.setTransform( // 水平 缩放、倾斜 - this.flip === "horizontal" ? -1 : 1, + this.flip === 'horizontal' ? -1 : 1, 0, // 垂直 倾斜、缩放 0, - this.flip === "vertical" ? -1 : 1, + this.flip === 'vertical' ? -1 : 1, // 坐标原点偏移 x y center.x, center.y, @@ -62,7 +62,7 @@ export abstract class BaseSprite { this.#animatKeyFrame = Object.entries(keyFrame).map(([k, val]) => { const numK = { from: 0, to: 100 }[k] ?? Number(k.slice(0, -1)); if (isNaN(numK) || numK > 100 || numK < 0) { - throw Error("keyFrame must between 0~100"); + throw Error('keyFrame must between 0~100'); } return [numK / 100, val]; }) as TAnimationKeyFrame; @@ -88,14 +88,14 @@ export abstract class BaseSprite { ); for (const k in updateProps) { switch (k) { - case "opacity": + case 'opacity': this.opacity = updateProps[k] as number; break; - case "x": - case "y": - case "w": - case "h": - case "angle": + case 'x': + case 'y': + case 'w': + case 'h': + case 'angle': this.rect[k] = updateProps[k] as number; break; } diff --git a/packages/av-cliper/src/chromakey.ts b/packages/av-cliper/src/chromakey.ts index 6c8ded92..3f6da4e3 100644 --- a/packages/av-cliper/src/chromakey.ts +++ b/packages/av-cliper/src/chromakey.ts @@ -72,7 +72,7 @@ function initShaderProgram( if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { throw Error( gl.getProgramInfoLog(shaderProgram) ?? - "Unable to initialize the shader program", + 'Unable to initialize the shader program', ); } @@ -93,7 +93,7 @@ function loadShader(gl: WebGLRenderingContext, type: number, source: string) { if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { const errMsg = gl.getShaderInfoLog(shader); gl.deleteShader(shader); - throw Error(errMsg ?? "An error occurred compiling the shaders"); + throw Error(errMsg ?? 'An error occurred compiling the shaders'); } return shader; @@ -111,7 +111,7 @@ function updateTexture( function initTexture(gl: WebGLRenderingContext) { const texture = gl.createTexture(); - if (texture == null) throw Error("Create WebGL texture error"); + if (texture == null) throw Error('Create WebGL texture error'); gl.bindTexture(gl.TEXTURE_2D, texture); // put a single pixel in the texture so we can use it immediately. @@ -157,40 +157,40 @@ function initCvs( } & IChromakeyOpts, ) { const cvs = - "document" in globalThis - ? globalThis.document.createElement("canvas") + 'document' in globalThis + ? globalThis.document.createElement('canvas') : new OffscreenCanvas(opts.width, opts.height); cvs.width = opts.width; cvs.height = opts.height; - const gl = cvs.getContext("webgl2", { + const gl = cvs.getContext('webgl2', { premultipliedAlpha: false, alpha: true, }) as WebGL2RenderingContext | null; - if (gl == null) throw Error("Cant create gl context"); + if (gl == null) throw Error('Cant create gl context'); const shaderProgram = initShaderProgram(gl, vertexShader, fragmentShader); gl.useProgram(shaderProgram); gl.uniform3fv( - gl.getUniformLocation(shaderProgram, "keyColor"), + gl.getUniformLocation(shaderProgram, 'keyColor'), opts.keyColor.map((v) => v / 255), ); gl.uniform1f( - gl.getUniformLocation(shaderProgram, "similarity"), + gl.getUniformLocation(shaderProgram, 'similarity'), opts.similarity, ); gl.uniform1f( - gl.getUniformLocation(shaderProgram, "smoothness"), + gl.getUniformLocation(shaderProgram, 'smoothness'), opts.smoothness, ); - gl.uniform1f(gl.getUniformLocation(shaderProgram, "spill"), opts.spill); + gl.uniform1f(gl.getUniformLocation(shaderProgram, 'spill'), opts.spill); const posBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(POINT_POS), gl.STATIC_DRAW); - const a_position = gl.getAttribLocation(shaderProgram, "a_position"); + const a_position = gl.getAttribLocation(shaderProgram, 'a_position'); gl.vertexAttribPointer( a_position, 2, @@ -208,7 +208,7 @@ function initCvs( new Float32Array(TEX_COORD_POS), gl.STATIC_DRAW, ); - const a_texCoord = gl.getAttribLocation(shaderProgram, "a_texCoord"); + const a_texCoord = gl.getAttribLocation(shaderProgram, 'a_texCoord'); gl.vertexAttribPointer( a_texCoord, 2, @@ -240,7 +240,7 @@ function getSourceWH(imgSource: TImgSource) { function getKeyColor(imgSource: TImgSource) { const cvs = new OffscreenCanvas(1, 1); - const ctx = cvs.getContext("2d")!; + const ctx = cvs.getContext('2d')!; ctx.drawImage(imgSource, 0, 0); const { data: [r, g, b], @@ -262,7 +262,7 @@ function getKeyColor(imgSource: TImgSource) { * } */ export const createChromakey = ( - opts: Omit & { + opts: Omit & { keyColor?: [number, number, number]; }, ) => { @@ -289,7 +289,7 @@ export const createChromakey = ( imgSource instanceof globalThis.VideoFrame ) { const rs = new VideoFrame(cvs, { - alpha: "keep", + alpha: 'keep', timestamp: imgSource.timestamp, duration: imgSource.duration ?? undefined, }); @@ -298,7 +298,7 @@ export const createChromakey = ( } return createImageBitmap(cvs, { - imageOrientation: imgSource instanceof ImageBitmap ? "flipY" : "none", + imageOrientation: imgSource instanceof ImageBitmap ? 'flipY' : 'none', }); }; }; 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 e8479400..f794ab41 100644 --- a/packages/av-cliper/src/clips/__tests__/audio-clip.test.ts +++ b/packages/av-cliper/src/clips/__tests__/audio-clip.test.ts @@ -1,6 +1,6 @@ -import { beforeEach, expect, test } from "vitest"; -import { AudioBufferMock } from "../../__tests__/mock"; -import { AudioClip, DEFAULT_AUDIO_CONF, concatAudioClip } from ".."; +import { beforeEach, expect, test } from 'vitest'; +import { AudioBufferMock } from '../../__tests__/mock'; +import { AudioClip, DEFAULT_AUDIO_CONF, concatAudioClip } from '..'; beforeEach(() => { AudioBufferMock.duration = 10; @@ -10,12 +10,12 @@ beforeEach(() => { ); }); -test("AudioClip tick", async () => { +test('AudioClip tick', async () => { const clip = new AudioClip(new ReadableStream()); await clip.ready; expect(await clip.tick(0)).toEqual({ audio: [new Float32Array(0), new Float32Array(0)], - state: "success", + state: 'success', }); // 每次取 30ms 的数据 await clip.tick(1000 * 30 * 1); @@ -23,16 +23,16 @@ test("AudioClip tick", async () => { audio: [chan0, chan1], state: s1, } = await clip.tick(1000 * 30 * 2); - expect(s1).toBe("success"); + expect(s1).toBe('success'); expect(chan0.length).toBe((DEFAULT_AUDIO_CONF.sampleRate / 1e3) * 30); expect(chan1.length).toBe((DEFAULT_AUDIO_CONF.sampleRate / 1e3) * 30); // 取第 11s 的数据 const { state: s2 } = await clip.tick(1e6 * 11); - expect(s2).toBe("done"); + expect(s2).toBe('done'); }); -test("AudioClip volume", async () => { +test('AudioClip volume', async () => { AudioBufferMock.getChannelData.mockReturnValueOnce( new Float32Array(Array(DEFAULT_AUDIO_CONF.sampleRate * 10).fill(1)), ); @@ -44,7 +44,7 @@ test("AudioClip volume", async () => { expect(Math.round(chan0[0] * 10) / 10).toBe(0.1); }); -test("AudioClip loop", async () => { +test('AudioClip loop', async () => { const clip = new AudioClip(new ReadableStream(), { loop: true }); await clip.ready; // 接近尾端 @@ -56,16 +56,16 @@ test("AudioClip loop", async () => { expect(chan0.length).toBe(DEFAULT_AUDIO_CONF.sampleRate * 2); }); -test("concatAudioClip", async () => { +test('concatAudioClip', async () => { const clip1 = new AudioClip(new ReadableStream()); const clip2 = new AudioClip(new ReadableStream()); // 两个clip 各10s,合成后总长度 20s const clip = await concatAudioClip([clip1, clip2]); const { audio, state: s1 } = await clip.tick(19 * 1e6); - expect(s1).toBe("success"); + expect(s1).toBe('success'); expect(audio.length).toBe(2); const { state: s2 } = await clip.tick(21 * 1e6); - expect(s2).toBe("done"); + expect(s2).toBe('done'); }); 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 1a7355ac..9cf31da6 100644 --- a/packages/av-cliper/src/clips/__tests__/embed-subtitles.test.ts +++ b/packages/av-cliper/src/clips/__tests__/embed-subtitles.test.ts @@ -1,6 +1,6 @@ -import { expect, test } from "vitest"; -import "../../__tests__/mock"; -import { EmbedSubtitlesClip } from "../embed-subtitles-clip"; +import { expect, test } from 'vitest'; +import '../../__tests__/mock'; +import { EmbedSubtitlesClip } from '../embed-subtitles-clip'; const txt1 = ` @@ -43,7 +43,7 @@ const txt1 = ` `; -test("EmbedSubtitles", async () => { +test('EmbedSubtitles', async () => { const es = new EmbedSubtitlesClip(txt1, { videoWidth: 1280, videoHeight: 720, @@ -60,7 +60,7 @@ test("EmbedSubtitles", async () => { // 100s 超出字幕时间 const { state } = await es.tick(100 * 1e6); - expect(state).toBe("done"); + expect(state).toBe('done'); }); const txt2 = ` @@ -70,7 +70,7 @@ const txt2 = ` 测试样本1-3s `; -test("EmbedSubtitles digital", async () => { +test('EmbedSubtitles digital', async () => { const es = new EmbedSubtitlesClip(txt2, { videoWidth: 1280, videoHeight: 720, diff --git a/packages/av-cliper/src/clips/audio-clip.ts b/packages/av-cliper/src/clips/audio-clip.ts index a6013fe6..21e934dd 100644 --- a/packages/av-cliper/src/clips/audio-clip.ts +++ b/packages/av-cliper/src/clips/audio-clip.ts @@ -2,9 +2,9 @@ import { concatPCMFragments, extractPCM4AudioBuffer, ringSliceFloat32Array, -} from "../av-utils"; -import { Log } from "../log"; -import { DEFAULT_AUDIO_CONF, IClip } from "./iclip"; +} from '../av-utils'; +import { Log } from '../log'; +import { DEFAULT_AUDIO_CONF, IClip } from './iclip'; interface IAudioClipOpts { loop?: boolean; @@ -14,7 +14,7 @@ interface IAudioClipOpts { export class AudioClip implements IClip { static ctx: AudioContext | null = null; - ready: IClip["ready"]; + ready: IClip['ready']; #meta = { // 微秒 @@ -69,7 +69,7 @@ export class AudioClip implements IClip { ? await parseStream2PCM(dataSource, AudioClip.ctx) : dataSource; - Log.info("Audio clip decoded complete:", performance.now() - tStart); + Log.info('Audio clip decoded complete:', performance.now() - tStart); const volume = this.#opts.volume; if (volume !== 1) { @@ -84,19 +84,19 @@ export class AudioClip implements IClip { this.#chan1Buf = pcm[1] ?? this.#chan0Buf; Log.info( - "Audio clip convert to AudioData, time:", + 'Audio clip convert to AudioData, time:', performance.now() - tStart, ); } async tick(time: number): Promise<{ audio: Float32Array[]; - state: "success" | "done"; + state: 'success' | 'done'; }> { - if (time < this.#ts) throw Error("time not allow rollback"); + if (time < this.#ts) throw Error('time not allow rollback'); if (!this.#opts.loop && time >= this.#meta.duration) { // 待观察:如果time跨度较大,返回done,理论上会丢失一些音频帧 - return { audio: [], state: "done" }; + return { audio: [], state: 'done' }; } const deltaTime = time - this.#ts; @@ -117,13 +117,13 @@ export class AudioClip implements IClip { ]; this.#frameOffset = endIdx; - return { audio, state: "success" }; + return { audio, state: 'success' }; } destroy(): void { this.#chan0Buf = new Float32Array(0); this.#chan1Buf = new Float32Array(0); - Log.info("---- audioclip destroy ----"); + Log.info('---- audioclip destroy ----'); } } diff --git a/packages/av-cliper/src/clips/embed-subtitles-clip.ts b/packages/av-cliper/src/clips/embed-subtitles-clip.ts index c470717b..51dc5b36 100644 --- a/packages/av-cliper/src/clips/embed-subtitles-clip.ts +++ b/packages/av-cliper/src/clips/embed-subtitles-clip.ts @@ -1,9 +1,9 @@ -import { IClip } from "./iclip"; +import { IClip } from './iclip'; interface IEmbedSubtitlesOpts { color?: string; textBgColor?: string | null; - type?: "srt"; + type?: 'srt'; fontFamily?: string; fontSize?: number; letterSpacing?: string | null; @@ -30,7 +30,7 @@ declare global { } export class EmbedSubtitlesClip implements IClip { - ready: IClip["ready"]; + ready: IClip['ready']; #subtitles: Array<{ start: number; @@ -39,14 +39,14 @@ export class EmbedSubtitlesClip implements IClip { }> = []; #opts: Required = { - color: "#FFF", + color: '#FFF', textBgColor: null, - type: "srt", + type: 'srt', fontSize: 30, letterSpacing: null, bottomOffset: 30, - fontFamily: "Noto Sans SC", - strokeStyle: "#000", + fontFamily: 'Noto Sans SC', + strokeStyle: '#000', lineWidth: null, lineCap: null, lineJoin: null, @@ -54,7 +54,7 @@ export class EmbedSubtitlesClip implements IClip { offsetX: 2, offsetY: 2, blur: 4, - color: "#000", + color: '#000', }, videoWidth: 1280, videoHeight: 720, @@ -74,7 +74,7 @@ export class EmbedSubtitlesClip implements IClip { end: end * 1e6, text, })); - if (this.#subtitles.length === 0) throw Error("No subtitles content"); + if (this.#subtitles.length === 0) throw Error('No subtitles content'); this.#opts = Object.assign(this.#opts, opts); // 如果需要绘制背景,则需要给文字添加边距 @@ -85,11 +85,11 @@ export class EmbedSubtitlesClip implements IClip { this.#opts; this.#lineHeight = fontSize + this.#linePadding * 2; this.#cvs = new OffscreenCanvas(videoWidth, videoHeight); - this.#ctx = this.#cvs.getContext("2d")!; + this.#ctx = this.#cvs.getContext('2d')!; this.#ctx.font = `${fontSize}px ${fontFamily}`; - this.#ctx.textAlign = "center"; - this.#ctx.textBaseline = "top"; - this.#ctx.letterSpacing = letterSpacing ?? "0px"; + this.#ctx.textAlign = 'center'; + this.#ctx.textBaseline = 'top'; + this.#ctx.letterSpacing = letterSpacing ?? '0px'; // 字幕的宽高 由视频画面内容决定 this.ready = Promise.resolve({ @@ -101,7 +101,7 @@ export class EmbedSubtitlesClip implements IClip { #renderTxt(txt: string) { const lines = txt - .split("\n") + .split('\n') .reverse() .map((t) => t.trim()); @@ -179,14 +179,14 @@ export class EmbedSubtitlesClip implements IClip { async tick(time: number): Promise<{ video?: VideoFrame; - state: "done" | "success"; + state: 'done' | 'success'; }> { if ( this.#lastVF != null && time >= this.#lastVF.timestamp && time <= this.#lastVF.timestamp + (this.#lastVF.duration ?? 0) ) { - return { video: this.#lastVF.clone(), state: "success" }; + return { video: this.#lastVF.clone(), state: 'success' }; } let i = 0; @@ -195,7 +195,7 @@ export class EmbedSubtitlesClip implements IClip { } const it = this.#subtitles[i] ?? this.#subtitles.at(-1); - if (time > it.end) return { state: "done" }; + if (time > it.end) return { state: 'done' }; if (time < it.start) { // 此时无字幕内容,清空画布 this.#ctx.clearRect(0, 0, this.#cvs.width, this.#cvs.height); @@ -207,7 +207,7 @@ export class EmbedSubtitlesClip implements IClip { this.#lastVF?.close(); this.#lastVF = vf; - return { video: vf.clone(), state: "success" }; + return { video: vf.clone(), state: 'success' }; } this.#renderTxt(it.text); @@ -219,7 +219,7 @@ export class EmbedSubtitlesClip implements IClip { this.#lastVF?.close(); this.#lastVF = vf; - return { video: vf.clone(), state: "success" }; + return { video: vf.clone(), state: 'success' }; } destroy() { @@ -270,7 +270,7 @@ function parseSrt(srt: string) { acc.push({ start: srtTimeToSeconds(match[1]), end: srtTimeToSeconds(match[2]), - text: "", + text: '', }); } diff --git a/packages/av-cliper/src/clips/iclip.ts b/packages/av-cliper/src/clips/iclip.ts index d2db177d..3afb1f98 100644 --- a/packages/av-cliper/src/clips/iclip.ts +++ b/packages/av-cliper/src/clips/iclip.ts @@ -6,13 +6,13 @@ export interface IClip { tick: (time: number) => Promise<{ video?: VideoFrame | ImageBitmap; audio?: Float32Array[]; - state: "done" | "success"; + state: 'done' | 'success'; }>; tickInterceptor?: ( tickTime: number, - tickRet: Awaited>, - ) => Promise>; + tickRet: Awaited>, + ) => Promise>; ready: Promise<{ width: number; height: number; duration: number }>; @@ -22,5 +22,5 @@ export interface IClip { export const DEFAULT_AUDIO_CONF = { sampleRate: 48000, channelCount: 2, - codec: "mp4a.40.2", + codec: 'mp4a.40.2', }; diff --git a/packages/av-cliper/src/clips/img-clip.ts b/packages/av-cliper/src/clips/img-clip.ts index 57ee5ce2..165bfff1 100644 --- a/packages/av-cliper/src/clips/img-clip.ts +++ b/packages/av-cliper/src/clips/img-clip.ts @@ -1,9 +1,9 @@ -import { decodeImg } from "../av-utils"; -import { Log } from "../log"; -import { IClip } from "./iclip"; +import { decodeImg } from '../av-utils'; +import { Log } from '../log'; +import { IClip } from './iclip'; export class ImgClip implements IClip { - ready: IClip["ready"]; + ready: IClip['ready']; #meta = { // 微秒 @@ -17,7 +17,7 @@ export class ImgClip implements IClip { #frames: VideoFrame[] = []; constructor( - dataSource: ImageBitmap | { type: "image/gif"; stream: ReadableStream }, + dataSource: ImageBitmap | { type: 'image/gif'; stream: ReadableStream }, ) { if (dataSource instanceof ImageBitmap) { this.#img = dataSource; @@ -42,24 +42,24 @@ export class ImgClip implements IClip { async #gifInit(stream: ReadableStream, type: string) { this.#frames = await decodeImg(stream, type); const firstVf = this.#frames[0]; - if (firstVf == null) throw Error("No frame available in gif"); + if (firstVf == null) throw Error('No frame available in gif'); this.#meta = { duration: this.#frames.reduce((acc, cur) => acc + (cur.duration ?? 0), 0), width: firstVf.codedWidth, height: firstVf.codedHeight, }; - Log.info("ImgClip ready:", this.#meta); + Log.info('ImgClip ready:', this.#meta); } async tick(time: number): Promise<{ video: ImageBitmap | VideoFrame; - state: "success"; + state: 'success'; }> { if (this.#img != null) { return { video: await createImageBitmap(this.#img), - state: "success", + state: 'success', }; } const tt = time % this.#meta.duration; @@ -69,12 +69,12 @@ export class ImgClip implements IClip { (f) => tt >= f.timestamp && tt <= f.timestamp + (f.duration ?? 0), ) ?? this.#frames[0] ).clone(), - state: "success", + state: 'success', }; } destroy(): void { - Log.info("ImgClip destroy"); + Log.info('ImgClip destroy'); this.#img?.close(); this.#frames.forEach((f) => f.close()); } diff --git a/packages/av-cliper/src/clips/index.ts b/packages/av-cliper/src/clips/index.ts index 3c62af29..90a6fc67 100644 --- a/packages/av-cliper/src/clips/index.ts +++ b/packages/av-cliper/src/clips/index.ts @@ -1,7 +1,7 @@ // 避免使用 DOM API 确保这些 Clip 能在 Worker 中运行 -export * from "./iclip"; -export * from "./mp4-clip"; -export * from "./img-clip"; -export * from "./audio-clip"; -export * from "./embed-subtitles-clip"; +export * from './iclip'; +export * from './mp4-clip'; +export * from './img-clip'; +export * from './audio-clip'; +export * from './embed-subtitles-clip'; diff --git a/packages/av-cliper/src/clips/mp4-clip.ts b/packages/av-cliper/src/clips/mp4-clip.ts index 0ff299a8..529ee1eb 100644 --- a/packages/av-cliper/src/clips/mp4-clip.ts +++ b/packages/av-cliper/src/clips/mp4-clip.ts @@ -3,10 +3,10 @@ import { concatFloat32Array, extractPCM4AudioData, sleep, -} from "../av-utils"; -import { Log } from "../log"; -import { demuxcode } from "../mp4-utils"; -import { DEFAULT_AUDIO_CONF, IClip } from "./iclip"; +} from '../av-utils'; +import { Log } from '../log'; +import { demuxcode } from '../mp4-utils'; +import { DEFAULT_AUDIO_CONF, IClip } from './iclip'; let CLIP_ID = 0; @@ -17,7 +17,7 @@ export class MP4Clip implements IClip { #ts = 0; - ready: IClip["ready"]; + ready: IClip['ready']; #destroyed = false; #decodeEnded = false; @@ -51,7 +51,7 @@ export class MP4Clip implements IClip { this.ready = new Promise((resolve) => { let lastVf: VideoFrame | null = null; this.#volume = - typeof opts.audio === "object" && "volume" in opts.audio + typeof opts.audio === 'object' && 'volume' in opts.audio ? opts.audio.volume : 1; this.#demuxcoder = demuxcode( @@ -75,7 +75,7 @@ export class MP4Clip implements IClip { audioSampleRate: DEFAULT_AUDIO_CONF.sampleRate, audioChanCount: DEFAULT_AUDIO_CONF.channelCount, }; - this.#log.info("MP4Clip info:", info); + this.#log.info('MP4Clip info:', info); resolve({ width: videoTrack.track_width, height: videoTrack.track_height, @@ -98,8 +98,8 @@ export class MP4Clip implements IClip { onAudioOutput: this.#audioData2PCMBuf, onComplete: () => { this.#decodeEnded = true; - this.#log.info("MP4Clip decode complete"); - if (lastVf == null) throw Error("mp4 parse error, no video frame"); + this.#log.info('MP4Clip decode complete'); + if (lastVf == null) throw Error('mp4 parse error, no video frame'); }, }, ); @@ -194,19 +194,19 @@ export class MP4Clip implements IClip { } // 默认直接返回 - tickInterceptor: NonNullable = async (_, tickRet) => + tickInterceptor: NonNullable = async (_, tickRet) => tickRet; async tick(time: number): Promise<{ video?: VideoFrame; audio: Float32Array[]; - state: "success" | "done"; + state: 'success' | 'done'; }> { - if (time < this.#ts) throw Error("time not allow rollback"); + if (time < this.#ts) throw Error('time not allow rollback'); if (this.#decodeEnded && time >= this.#meta.duration) { return await this.tickInterceptor(time, { audio: [], - state: "done", + state: 'done', }); } @@ -218,25 +218,25 @@ export class MP4Clip implements IClip { if (video == null) { return await this.tickInterceptor(time, { audio, - state: "success", + state: 'success', }); } return await this.tickInterceptor(time, { video, audio, - state: "success", + state: 'success', }); } destroy(): void { if (this.#destroyed) return; this.#log.info( - "MP4Clip destroy, ts:", + 'MP4Clip destroy, ts:', this.#ts, - ", remainder frame count:", + ', remainder frame count:', this.#videoFrames.length, - ", decodeEnded:", + ', decodeEnded:', this.#decodeEnded, ); this.#destroyed = true; diff --git a/packages/av-cliper/src/combinator.ts b/packages/av-cliper/src/combinator.ts index 8bb8ba7f..85ab60c9 100644 --- a/packages/av-cliper/src/combinator.ts +++ b/packages/av-cliper/src/combinator.ts @@ -1,9 +1,9 @@ -import { OffscreenSprite } from "./offscreen-sprite"; -import { file2stream, recodemux } from "./mp4-utils"; -import { Log } from "./log"; -import { mixinPCM, sleep, throttle } from "./av-utils"; -import { EventTool } from "./event-tool"; -import { DEFAULT_AUDIO_CONF } from "./clips"; +import { OffscreenSprite } from './offscreen-sprite'; +import { file2stream, recodemux } from './mp4-utils'; +import { Log } from './log'; +import { mixinPCM, sleep, throttle } from './av-utils'; +import { EventTool } from './event-tool'; +import { DEFAULT_AUDIO_CONF } from './clips'; interface IComItem { offset: number; @@ -59,7 +59,7 @@ const encoderIdle = (() => { export class Combinator { static async isSupported( args = { - videoCodec: "avc1.42E032", + videoCodec: 'avc1.42E032', }, ): Promise { return ( @@ -114,20 +114,20 @@ export class Combinator { const { width, height } = opts; this.#cvs = new OffscreenCanvas(width, height); // this.#cvs = document.querySelector('#canvas') as HTMLCanvasElement - const ctx = this.#cvs.getContext("2d", { alpha: false }); - if (ctx == null) throw Error("Can not create 2d offscreen context"); + const ctx = this.#cvs.getContext('2d', { alpha: false }); + if (ctx == null) throw Error('Can not create 2d offscreen context'); this.#ctx = ctx; - this.#opts = Object.assign({ bgColor: "#000" }, opts); + this.#opts = Object.assign({ bgColor: '#000' }, opts); this.#remux = recodemux({ video: { width, height, expectFPS: 30, - codec: opts.videoCodec ?? "avc1.42E032", + codec: opts.videoCodec ?? 'avc1.42E032', }, audio: { - codec: "aac", + codec: 'aac', sampleRate: DEFAULT_AUDIO_CONF.sampleRate, channelCount: DEFAULT_AUDIO_CONF.channelCount, }, @@ -141,9 +141,9 @@ export class Combinator { sprite: OffscreenSprite, opts: { offset?: number; duration?: number; main?: boolean } = {}, ): Promise { - this.#log.info("Combinator add sprite:", sprite.name); + this.#log.info('Combinator add sprite:', sprite.name); await sprite.ready; - this.#log.info("Combinator add sprite ready:", sprite.name); + this.#log.info('Combinator add sprite ready:', sprite.name); this.#comItems.push({ sprite, offset: (opts.offset ?? 0) * 1e6, @@ -154,7 +154,7 @@ export class Combinator { } output(): ReadableStream { - if (this.#comItems.length === 0) throw Error("No clip added"); + if (this.#comItems.length === 0) throw Error('No clip added'); const mainItem = this.#comItems.find((it) => it.main); // 最大时间,优先取 main sprite,不存在则取最大值 @@ -165,7 +165,7 @@ export class Combinator { if (maxTime === Infinity) { throw Error( - "Unable to determine the end time, please specify a main sprite, or limit the duration of ImgClip, AudioCli", + 'Unable to determine the end time, please specify a main sprite, or limit the duration of ImgClip, AudioCli', ); } // 主视频(main)的 videoTrack duration 值为 0 @@ -179,16 +179,16 @@ export class Combinator { const stopReCodeMux = this.#run( maxTime, (prog) => { - this.#log.debug("OutputProgress:", prog); - this.#evtTool.emit("OutputProgress", prog); + this.#log.debug('OutputProgress:', prog); + this.#evtTool.emit('OutputProgress', prog); }, async () => { await this.#remux.flush(); this.#log.info( - "===== output ended =====, cost:", + '===== output ended =====, cost:', performance.now() - starTime, ); - this.#evtTool.emit("OutputProgress", 1); + this.#evtTool.emit('OutputProgress', 1); this.destroy(); }, ); @@ -292,7 +292,7 @@ export class Combinator { numberOfChannels: DEFAULT_AUDIO_CONF.channelCount, numberOfFrames: data.length / DEFAULT_AUDIO_CONF.channelCount, sampleRate: DEFAULT_AUDIO_CONF.sampleRate, - format: "f32-planar", + format: 'f32-planar', data, }), ); @@ -352,7 +352,7 @@ function createAudioPlaceholder( numberOfChannels: DEFAULT_AUDIO_CONF.channelCount, numberOfFrames: frameCnt, sampleRate: sampleRate, - format: "f32-planar", + format: 'f32-planar', data: new Float32Array(frameCnt * 2), }); } diff --git a/packages/av-cliper/src/dom-utils.ts b/packages/av-cliper/src/dom-utils.ts index 1cc74efe..41d492e3 100644 --- a/packages/av-cliper/src/dom-utils.ts +++ b/packages/av-cliper/src/dom-utils.ts @@ -5,7 +5,7 @@ export function createEl(tagName: string): HTMLElement { } export function renderTxt2Img(txt: string, cssText: string): HTMLImageElement { - const div = createEl("div"); + const div = createEl('div'); div.style.cssText = `${cssText} visibility: hidden; position: fixed;`; div.textContent = txt; document.body.appendChild(div); @@ -13,7 +13,7 @@ export function renderTxt2Img(txt: string, cssText: string): HTMLImageElement { const { width, height } = div.getBoundingClientRect(); // 计算出 rect,立即从dom移除 div.remove(); - div.style.visibility = "visible"; + div.style.visibility = 'visible'; const img = new Image(); img.width = width; @@ -25,9 +25,9 @@ export function renderTxt2Img(txt: string, cssText: string): HTMLImageElement { ` - .replace(/\n/g, "") - .replace(/\t/g, "") - .replace(/#/g, "%23"); + .replace(/\n/g, '') + .replace(/\t/g, '') + .replace(/#/g, '%23'); img.src = `data:image/svg+xml;charset=utf-8,${svgStr}`; return img; @@ -42,7 +42,7 @@ export async function renderTxt2ImgBitmap( imgEl.onload = resolve; }); const cvs = new OffscreenCanvas(imgEl.width, imgEl.height); - const ctx = cvs.getContext("2d"); + const ctx = cvs.getContext('2d'); ctx?.drawImage(imgEl, 0, 0, imgEl.width, imgEl.height); return await createImageBitmap(cvs); } diff --git a/packages/av-cliper/src/index.ts b/packages/av-cliper/src/index.ts index 17c02eb2..7921bd53 100644 --- a/packages/av-cliper/src/index.ts +++ b/packages/av-cliper/src/index.ts @@ -1,12 +1,12 @@ -export * from "./dom-utils"; -export * from "./av-utils"; -export * from "./mp4-utils"; -export { createChromakey } from "./chromakey"; +export * from './dom-utils'; +export * from './av-utils'; +export * from './mp4-utils'; +export { createChromakey } from './chromakey'; -export * from "./rect"; -export * from "./clips"; -export { OffscreenSprite } from "./offscreen-sprite"; -export { BaseSprite } from "./base-sprite"; -export { Combinator } from "./combinator"; +export * from './rect'; +export * from './clips'; +export { OffscreenSprite } from './offscreen-sprite'; +export { BaseSprite } from './base-sprite'; +export { Combinator } from './combinator'; -export * from "./log"; +export * from './log'; diff --git a/packages/av-cliper/src/log.ts b/packages/av-cliper/src/log.ts index 6048a8ea..f48e89e8 100644 --- a/packages/av-cliper/src/log.ts +++ b/packages/av-cliper/src/log.ts @@ -41,6 +41,6 @@ if (import.meta.env.DEV) { Log.setLogLevel(Log.debug); } -if (import.meta.env.MODE === "test") { +if (import.meta.env.MODE === 'test') { Log.setLogLevel(Log.warn); } diff --git a/packages/av-cliper/src/mp4-utils/__tests__/mp4-utils.test.ts b/packages/av-cliper/src/mp4-utils/__tests__/mp4-utils.test.ts index b9f414a1..d231d426 100644 --- a/packages/av-cliper/src/mp4-utils/__tests__/mp4-utils.test.ts +++ b/packages/av-cliper/src/mp4-utils/__tests__/mp4-utils.test.ts @@ -1,14 +1,14 @@ -import "../../__tests__/mock"; -import { beforeAll, describe, expect, test, vi } from "vitest"; -import mp4box from "@webav/mp4box.js"; -import { file2stream } from ".."; +import '../../__tests__/mock'; +import { beforeAll, describe, expect, test, vi } from 'vitest'; +import mp4box from '@webav/mp4box.js'; +import { file2stream } from '..'; beforeAll(() => { vi.useFakeTimers(); }); -describe("file2stream", () => { - test("enqueue data to stream", () => { +describe('file2stream', () => { + test('enqueue data to stream', () => { const file = mp4box.createFile(); file.boxes = []; @@ -31,13 +31,13 @@ describe("file2stream", () => { expect([...file.boxes]).toEqual(Array(5).fill(undefined)); }); - test("stop stream", () => { + test('stop stream', () => { const file = mp4box.createFile(); file.boxes = Array(5) .fill(0) .map(() => ({ write: vi.fn(), data: new ArrayBuffer(0) })); - vi.spyOn(file, "flush"); - vi.spyOn(global, "clearInterval"); + vi.spyOn(file, 'flush'); + vi.spyOn(global, 'clearInterval'); const { stop, stream } = file2stream(file, 500); stop(); @@ -49,13 +49,13 @@ describe("file2stream", () => { expect(global.clearInterval).toBeCalled(); }); - test("cancel stream", () => { + test('cancel stream', () => { const file = mp4box.createFile(); file.boxes = Array(5) .fill(0) .map(() => ({ write: vi.fn(), data: new ArrayBuffer(0) })); - vi.spyOn(file, "flush"); - vi.spyOn(global, "clearInterval"); + vi.spyOn(file, 'flush'); + vi.spyOn(global, 'clearInterval'); const spyCancel = vi.fn(); const { stream } = file2stream(file, 500, spyCancel); diff --git a/packages/av-cliper/src/mp4-utils/index.ts b/packages/av-cliper/src/mp4-utils/index.ts index 44654961..b50f329e 100644 --- a/packages/av-cliper/src/mp4-utils/index.ts +++ b/packages/av-cliper/src/mp4-utils/index.ts @@ -5,8 +5,8 @@ import mp4box, { MP4Sample, SampleOpts, TrakBoxParser, -} from "@webav/mp4box.js"; -import { Log } from "../log"; +} from '@webav/mp4box.js'; +import { Log } from '../log'; import { autoReadStream, extractPCM4AudioData, @@ -14,13 +14,13 @@ import { mixinPCM, ringSliceFloat32Array, concatPCMFragments, -} from "../av-utils"; -import { DEFAULT_AUDIO_CONF } from "../clips"; -import { EventTool } from "../event-tool"; -import { SampleTransform } from "./sample-transform"; -import { extractFileConfig, sample2ChunkOpts } from "./mp4box-utils"; +} from '../av-utils'; +import { DEFAULT_AUDIO_CONF } from '../clips'; +import { EventTool } from '../event-tool'; +import { SampleTransform } from './sample-transform'; +import { extractFileConfig, sample2ChunkOpts } from './mp4box-utils'; -export { MP4Previewer } from "./mp4-previewer"; +export { MP4Previewer } from './mp4-previewer'; type TCleanFn = () => void; @@ -32,7 +32,7 @@ interface IWorkerOpts { codec: string; }; audio: { - codec: "opus" | "aac"; + codec: 'opus' | 'aac'; sampleRate: number; channelCount: number; } | null; @@ -76,22 +76,22 @@ export function demuxcode( stream.pipeThrough(new SampleTransform()), { onDone: async () => { - Log.info("demuxcode stream done"); - if (mp4Info == null) throw Error("MP4 demux unready"); + Log.info('demuxcode stream done'); + if (mp4Info == null) throw Error('MP4 demux unready'); try { await Promise.all([ - vdecoder.state === "configured" ? vdecoder.flush() : null, - adecoder.state === "configured" ? adecoder.flush() : null, + vdecoder.state === 'configured' ? vdecoder.flush() : null, + adecoder.state === 'configured' ? adecoder.flush() : null, ]); - Log.info("demuxcode decode done"); + Log.info('demuxcode decode done'); cbs.onComplete(); } catch (err) { Log.info(err); } }, onChunk: async ({ chunkType, data }) => { - if (chunkType === "ready") { - Log.info("demuxcode chunk ready, info:", data); + if (chunkType === 'ready') { + Log.info('demuxcode chunk ready, info:', data); mp4File = data.file; mp4Info = data.info; const { videoDecoderConf, audioDecoderConf } = extractFileConfig( @@ -103,7 +103,7 @@ export function demuxcode( vdecoder.configure(videoDecoderConf); } else { throw new Error( - "MP4 file does not include a video track or uses an unsupported codec", + 'MP4 file does not include a video track or uses an unsupported codec', ); } if (opts.audio && audioDecoderConf != null) { @@ -112,7 +112,7 @@ export function demuxcode( cbs.onReady(data.info); return; - } else if (chunkType === "samples") { + } else if (chunkType === 'samples') { const { id: curId, type, samples } = data; for (let i = 0; i < samples.length; i += 1) { const s = samples[i]; @@ -122,7 +122,7 @@ export function demuxcode( // 跳过裁剪时间区间外的sample,无需解码 if (cts < opts.start || cts > opts.end) continue; - if (type === "video") { + if (type === 'video') { if (firstVideoSamp && i === 0 && s.cts !== 0) { // 兼容某些视频首帧 有偏移 s.cts = 0; @@ -140,7 +140,7 @@ export function demuxcode( } } vdecoder.decode(new EncodedVideoChunk(sample2ChunkOpts(s))); - } else if (type === "audio" && opts.audio) { + } else if (type === 'audio' && opts.audio) { adecoder.decode(new EncodedAudioChunk(sample2ChunkOpts(s))); } } @@ -173,17 +173,17 @@ export function recodemux(opts: IWorkerOpts): { mp4file: MP4File; getEecodeQueueSize: () => number; } { - Log.info("recodemux opts:", opts); + Log.info('recodemux opts:', opts); const mp4file = mp4box.createFile(); // 音视频轨道必须同时创建, 保存在 moov 中 const avSyncEvtTool = new EventTool< - Record<"VideoReady" | "AudioReady", () => void> + Record<'VideoReady' | 'AudioReady', () => void> >(); const vEncoder = encodeVideoTrack(opts, mp4file, avSyncEvtTool); let aEncoder: AudioEncoder | null = null; if (opts.audio == null) { - avSyncEvtTool.emit("AudioReady"); + avSyncEvtTool.emit('AudioReady'); } else { aEncoder = encodeAudioTrack(opts.audio, mp4file, avSyncEvtTool); } @@ -205,15 +205,15 @@ export function recodemux(opts: IWorkerOpts): { getEecodeQueueSize: () => vEncoder.encodeQueueSize, flush: async () => { await Promise.all([ - vEncoder.state === "configured" ? vEncoder.flush() : null, - aEncoder?.state === "configured" ? aEncoder.flush() : null, + vEncoder.state === 'configured' ? vEncoder.flush() : null, + aEncoder?.state === 'configured' ? aEncoder.flush() : null, ]); return; }, close: () => { avSyncEvtTool.destroy(); - if (vEncoder.state === "configured") vEncoder.close(); - if (aEncoder?.state === "configured") aEncoder.close(); + if (vEncoder.state === 'configured') vEncoder.close(); + if (aEncoder?.state === 'configured') aEncoder.close(); }, mp4file, }; @@ -222,21 +222,21 @@ export function recodemux(opts: IWorkerOpts): { function encodeVideoTrack( opts: IWorkerOpts, mp4File: MP4File, - avSyncEvtTool: EventTool void>>, + avSyncEvtTool: EventTool void>>, ): VideoEncoder { const videoTrackOpts = { // 微秒 timescale: 1e6, width: opts.video.width, height: opts.video.height, - brands: ["isom", "iso2", "avc1", "mp42", "mp41"], + brands: ['isom', 'iso2', 'avc1', 'mp42', 'mp41'], avcDecoderConfigRecord: null as ArrayBuffer | undefined | null, }; let trackId: number; let cache: EncodedVideoChunk[] = []; let audioReady = false; - avSyncEvtTool.once("AudioReady", () => { + avSyncEvtTool.once('AudioReady', () => { audioReady = true; cache.forEach((c) => { const s = chunk2MP4SampleOpts(c); @@ -249,8 +249,8 @@ function encodeVideoTrack( videoTrackOpts.avcDecoderConfigRecord = meta.decoderConfig ?.description as ArrayBuffer; trackId = mp4File.addTrack(videoTrackOpts); - avSyncEvtTool.emit("VideoReady"); - Log.info("VideoEncoder, video track ready, trackId:", trackId); + avSyncEvtTool.emit('VideoReady'); + Log.info('VideoEncoder, video track ready, trackId:', trackId); } if (audioReady) { @@ -283,9 +283,9 @@ function createVideoEncoder( width: videoOpts.width, height: videoOpts.height, // H264 不支持背景透明度 - alpha: "discard", + alpha: 'discard', // macos 自带播放器只支持avc - avc: { format: "avc" }, + avc: { format: 'avc' }, // mp4box.js 无法解析 annexb 的 mimeCodec ,只会显示 avc1 // avc: { format: 'annexb' } }); @@ -293,22 +293,22 @@ function createVideoEncoder( } function encodeAudioTrack( - audioOpts: NonNullable, + audioOpts: NonNullable, mp4File: MP4File, - avSyncEvtTool: EventTool void>>, + avSyncEvtTool: EventTool void>>, ): AudioEncoder { const audioTrackOpts = { timescale: 1e6, samplerate: audioOpts.sampleRate, channel_count: audioOpts.channelCount, - hdlr: "soun", - type: audioOpts.codec === "aac" ? "mp4a" : "Opus", + hdlr: 'soun', + type: audioOpts.codec === 'aac' ? 'mp4a' : 'Opus', }; let trackId = 0; let cache: EncodedAudioChunk[] = []; let videoReady = false; - avSyncEvtTool.once("VideoReady", () => { + avSyncEvtTool.once('VideoReady', () => { videoReady = true; cache.forEach((c) => { const s = chunk2MP4SampleOpts(c); @@ -327,8 +327,8 @@ function encodeAudioTrack( ...audioTrackOpts, description: desc == null ? undefined : createESDSBox(desc), }); - avSyncEvtTool.emit("AudioReady"); - Log.info("AudioEncoder, audio track ready, trackId:", trackId); + avSyncEvtTool.emit('AudioReady'); + Log.info('AudioEncoder, audio track ready, trackId:', trackId); } if (videoReady) { @@ -340,7 +340,7 @@ function encodeAudioTrack( }, }); encoder.configure({ - codec: audioOpts.codec === "aac" ? DEFAULT_AUDIO_CONF.codec : "opus", + codec: audioOpts.codec === 'aac' ? DEFAULT_AUDIO_CONF.codec : 'opus', sampleRate: audioOpts.sampleRate, numberOfChannels: audioOpts.channelCount, bitrate: 128_000, @@ -361,7 +361,7 @@ export function _deprecated_stream2file(stream: ReadableStream): { while (!stoped) { const { done, value } = await reader.read(); if (done) { - Log.info("stream source read done"); + Log.info('stream source read done'); file.flush(); file.onFlush?.(); return; @@ -497,7 +497,7 @@ function mp4File2OPFSFile(inMP4File: MP4File): () => Promise { function moovBoxReady() { if (moovPrevBoxes.length > 0) return true; - const moovIdx = boxes.findIndex((box) => box.type === "moov"); + const moovIdx = boxes.findIndex((box) => box.type === 'moov'); if (moovIdx === -1) return false; moovPrevBoxes = boxes.slice(0, moovIdx + 1); @@ -533,7 +533,7 @@ function mp4File2OPFSFile(inMP4File: MP4File): () => Promise { let stoped = false; return async () => { - if (stoped) throw Error("File exported"); + if (stoped) throw Error('File exported'); stoped = true; await initPromise; @@ -544,7 +544,7 @@ function mp4File2OPFSFile(inMP4File: MP4File): () => Promise { await write2TmpFile(); await tmpFileWriter?.close(); - const moov = moovPrevBoxes.find((box) => box.type === "moov") as + const moov = moovPrevBoxes.find((box) => box.type === 'moov') as | typeof inMP4File.moov | undefined; if (moov == null) return null; @@ -595,7 +595,7 @@ function chunk2MP4SampleOpts( duration: chunk.duration ?? 0, dts, cts: dts, - is_sync: chunk.type === "key", + is_sync: chunk.type === 'key', data: buf, }; } @@ -607,13 +607,13 @@ function chunk2MP4SampleOpts( export async function fastConcatMP4( streams: ReadableStream[], ): Promise> { - Log.info("fastConcatMP4, streams len:", streams.length); + Log.info('fastConcatMP4, streams len:', streams.length); const outfile = mp4box.createFile(); const dumpFile = mp4File2OPFSFile(outfile); await concatStreamsToMP4BoxFile(streams, outfile); const opfsFile = await dumpFile(); - if (opfsFile == null) throw Error("Can not generate file from streams"); + if (opfsFile == null) throw Error('Can not generate file from streams'); return opfsFile.stream(); } @@ -636,7 +636,7 @@ async function concatStreamsToMP4BoxFile( autoReadStream(stream.pipeThrough(new SampleTransform()), { onDone: resolve, onChunk: async ({ chunkType, data }) => { - if (chunkType === "ready") { + if (chunkType === 'ready') { const { videoTrackConf, audioTrackConf } = extractFileConfig( data.file, data.info, @@ -648,11 +648,11 @@ async function concatStreamsToMP4BoxFile( if (aTrackId === 0 && audioTrackConf != null) { aTrackId = outfile.addTrack(audioTrackConf); } - } else if (chunkType === "samples") { + } else if (chunkType === 'samples') { const { id: curId, type, samples } = data; - const trackId = type === "video" ? vTrackId : aTrackId; - const offsetDTS = type === "video" ? vDTS : aDTS; - const offsetCTS = type === "video" ? vCTS : aCTS; + const trackId = type === 'video' ? vTrackId : aTrackId; + const offsetDTS = type === 'video' ? vDTS : aDTS; + const offsetCTS = type === 'video' ? vCTS : aCTS; samples.forEach((s) => { outfile.addSample(trackId, s.data, { @@ -666,9 +666,9 @@ async function concatStreamsToMP4BoxFile( const lastSamp = samples.at(-1); if (lastSamp == null) return; - if (type === "video") { + if (type === 'video') { lastVSamp = lastSamp; - } else if (type === "audio") { + } else if (type === 'audio') { lastASamp = lastSamp; } } @@ -699,12 +699,12 @@ export async function mp4StreamToOPFSFile( const dumpFile = mp4File2OPFSFile(outfile); await concatStreamsToMP4BoxFile([stream], outfile); const opfsFile = await dumpFile(); - if (opfsFile == null) throw Error("Can not generate file from stream"); + if (opfsFile == null) throw Error('Can not generate file from stream'); return opfsFile; } function createMP4AudioSampleDecoder( - adConf: Parameters[0], + adConf: Parameters[0], ) { let cacheAD: AudioData[] = []; const adDecoder = new AudioDecoder({ @@ -720,7 +720,7 @@ function createMP4AudioSampleDecoder( ss.forEach((s) => { adDecoder.decode( new EncodedAudioChunk({ - type: s.is_sync ? "key" : "delta", + type: s.is_sync ? 'key' : 'delta', timestamp: (1e6 * s.cts) / s.timescale, duration: (1e6 * s.duration) / s.timescale, data: s.data, @@ -744,7 +744,7 @@ function createMP4AudioSampleDecoder( // 音频编码与解码API有很大区别, // 是因为编码中途调用 AudioEncoder.flush ,会导致声音听起来卡顿 function createMP4AudioSampleEncoder( - aeConf: Parameters[0], + aeConf: Parameters[0], onOutput: (s: ReturnType) => void, ) { const adEncoder = new AudioEncoder({ @@ -769,7 +769,7 @@ function createMP4AudioSampleEncoder( numberOfChannels: aeConf.numberOfChannels, numberOfFrames: data.length / aeConf.numberOfChannels, sampleRate: aeConf.sampleRate, - format: "f32-planar", + format: 'f32-planar', data, }); } @@ -821,7 +821,7 @@ export function mixinMP4AndAudio( loop: boolean; }, ) { - Log.info("mixinMP4AndAudio, opts:", { + Log.info('mixinMP4AndAudio, opts:', { volume: audio.volume, loop: audio.loop, }); @@ -851,7 +851,7 @@ export function mixinMP4AndAudio( stopOut(); }, onChunk: async ({ chunkType, data }) => { - if (chunkType === "ready") { + if (chunkType === 'ready') { const { videoTrackConf, audioTrackConf, audioDecoderConf } = extractFileConfig(data.file, data.info); if (vTrackId === 0 && videoTrackConf != null) { @@ -862,9 +862,9 @@ export function mixinMP4AndAudio( timescale: 1e6, samplerate: sampleRate, channel_count: DEFAULT_AUDIO_CONF.channelCount, - hdlr: "soun", - name: "SoundHandler", - type: "mp4a", + hdlr: 'soun', + name: 'SoundHandler', + type: 'mp4a', }; if (aTrackId === 0) { aTrackId = outfile.addTrack(safeAudioTrackConf); @@ -884,7 +884,7 @@ export function mixinMP4AndAudio( audioSampleEncoder = createMP4AudioSampleEncoder( audioDecoderConf ?? { codec: - safeAudioTrackConf.type === "mp4a" + safeAudioTrackConf.type === 'mp4a' ? DEFAULT_AUDIO_CONF.codec : safeAudioTrackConf.type, numberOfChannels: safeAudioTrackConf.channel_count, @@ -892,16 +892,16 @@ export function mixinMP4AndAudio( }, (s) => outfile.addSample(aTrackId, s.data, s), ); - } else if (chunkType === "samples") { + } else if (chunkType === 'samples') { const { id, type, samples } = data; - if (type === "video") { + if (type === 'video') { samples.forEach((s) => outfile.addSample(id, s.data, s)); if (!mp4HasAudio) await addInputAudio2Track(samples); return; } - if (type === "audio") await mixinAudioSampleAndInputPCM(samples); + if (type === 'audio') await mixinAudioSampleAndInputPCM(samples); } }, }); diff --git a/packages/av-cliper/src/mp4-utils/mp4-previewer.ts b/packages/av-cliper/src/mp4-utils/mp4-previewer.ts index da314420..3608c194 100644 --- a/packages/av-cliper/src/mp4-utils/mp4-previewer.ts +++ b/packages/av-cliper/src/mp4-utils/mp4-previewer.ts @@ -1,15 +1,15 @@ -import { MP4File, MP4Info, MP4Sample } from "@webav/mp4box.js"; -import { autoReadStream } from "../av-utils"; -import { Log } from "../log"; -import { extractFileConfig, sample2ChunkOpts } from "./mp4box-utils"; -import { file } from "opfs-tools"; -import { SampleTransform } from "./sample-transform"; +import { MP4File, MP4Info, MP4Sample } from '@webav/mp4box.js'; +import { autoReadStream } from '../av-utils'; +import { Log } from '../log'; +import { extractFileConfig, sample2ChunkOpts } from './mp4box-utils'; +import { file } from 'opfs-tools'; +import { SampleTransform } from './sample-transform'; export class MP4Previewer { #ready: Promise; #videoSamples: Array< - Omit & { + Omit & { offset: number; timeEnd: number; data: null; @@ -35,13 +35,13 @@ export class MP4Previewer { let mp4boxFile: MP4File | null = null; autoReadStream(stream.pipeThrough(new SampleTransform()), { onChunk: async ({ chunkType, data }): Promise => { - if (chunkType === "ready") { + if (chunkType === 'ready') { const { videoDecoderConf, videoTrackConf } = extractFileConfig( data.file, data.info, ); if (videoDecoderConf == null || videoTrackConf == null) { - reject("Unsupported codec"); + reject('Unsupported codec'); return; } mp4Info = { @@ -52,12 +52,12 @@ export class MP4Previewer { mp4boxFile = data.file; const { width, height } = data.info.videoTracks[0].video; this.#cvs = new OffscreenCanvas(width, height); - this.#ctx = this.#cvs.getContext("2d"); + this.#ctx = this.#cvs.getContext('2d'); this.#wrapDecoder = wrapVideoDecoder(videoDecoderConf); } - if (chunkType === "samples") { - if (data.type === "video") { + if (chunkType === 'samples') { + if (data.type === 'video') { for (const s of data.samples) { this.#videoSamples.push({ ...s, @@ -75,7 +75,7 @@ export class MP4Previewer { onDone: async () => { await writer.close(); if (mp4Info == null) { - reject("Parse failed"); + reject('Parse failed'); return; } resolve(mp4Info); diff --git a/packages/av-cliper/src/mp4-utils/mp4box-utils.ts b/packages/av-cliper/src/mp4-utils/mp4box-utils.ts index aeaae1c5..9dea5411 100644 --- a/packages/av-cliper/src/mp4-utils/mp4box-utils.ts +++ b/packages/av-cliper/src/mp4-utils/mp4box-utils.ts @@ -5,25 +5,25 @@ import mp4box, { MP4Info, TrakBoxParser, VideoTrackOpts, -} from "@webav/mp4box.js"; -import { DEFAULT_AUDIO_CONF } from "../clips"; +} from '@webav/mp4box.js'; +import { DEFAULT_AUDIO_CONF } from '../clips'; export function extractFileConfig(file: MP4File, info: MP4Info) { const vTrack = info.videoTracks[0]; const rs: { videoTrackConf?: VideoTrackOpts; - videoDecoderConf?: Parameters[0]; + videoDecoderConf?: Parameters[0]; audioTrackConf?: AudioTrackOpts; - audioDecoderConf?: Parameters[0]; + audioDecoderConf?: Parameters[0]; } = {}; if (vTrack != null) { const videoDesc = parseVideoCodecDesc(file.getTrackById(vTrack.id)).buffer; - const { descKey, type } = vTrack.codec.startsWith("avc1") - ? { descKey: "avcDecoderConfigRecord", type: "avc1" } - : vTrack.codec.startsWith("hvc1") - ? { descKey: "hevcDecoderConfigRecord", type: "hvc1" } - : { descKey: "", type: "" }; - if (descKey !== "") { + const { descKey, type } = vTrack.codec.startsWith('avc1') + ? { descKey: 'avcDecoderConfigRecord', type: 'avc1' } + : vTrack.codec.startsWith('hvc1') + ? { descKey: 'hevcDecoderConfigRecord', type: 'hvc1' } + : { descKey: '', type: '' }; + if (descKey !== '') { rs.videoTrackConf = { timescale: vTrack.timescale, duration: vTrack.duration, @@ -49,12 +49,12 @@ export function extractFileConfig(file: MP4File, info: MP4Info) { timescale: aTrack.timescale, samplerate: aTrack.audio.sample_rate, channel_count: aTrack.audio.channel_count, - hdlr: "soun", - type: aTrack.codec.startsWith("mp4a") ? "mp4a" : aTrack.codec, + hdlr: 'soun', + type: aTrack.codec.startsWith('mp4a') ? 'mp4a' : aTrack.codec, description: getESDSBoxFromMP4File(file), }; rs.audioDecoderConf = { - codec: aTrack.codec.startsWith("mp4a") + codec: aTrack.codec.startsWith('mp4a') ? DEFAULT_AUDIO_CONF.codec : aTrack.codec, numberOfChannels: aTrack.audio.channel_count, @@ -79,10 +79,10 @@ function parseVideoCodecDesc(track: TrakBoxParser): Uint8Array { return new Uint8Array(stream.buffer.slice(8)); // Remove the box header. } } - throw Error("avcC, hvcC or VPX not found"); + throw Error('avcC, hvcC or VPX not found'); } -function getESDSBoxFromMP4File(file: MP4File, codec = "mp4a") { +function getESDSBoxFromMP4File(file: MP4File, codec = 'mp4a') { const mp4aBox = file.moov?.traks .map((t) => t.mdia.minf.stbl.stsd.entries) .flat() @@ -99,7 +99,7 @@ export function sample2ChunkOpts(s: { data: ArrayBuffer; }): EncodedAudioChunkInit | EncodedVideoChunkInit { return { - type: (s.is_sync ? "key" : "delta") as EncodedVideoChunkType, + type: (s.is_sync ? 'key' : 'delta') as EncodedVideoChunkType, timestamp: (1e6 * s.cts) / s.timescale, duration: (1e6 * s.duration) / s.timescale, data: s.data, diff --git a/packages/av-cliper/src/mp4-utils/sample-transform.ts b/packages/av-cliper/src/mp4-utils/sample-transform.ts index 32315ebf..ec9a4eaf 100644 --- a/packages/av-cliper/src/mp4-utils/sample-transform.ts +++ b/packages/av-cliper/src/mp4-utils/sample-transform.ts @@ -3,8 +3,8 @@ import mp4box, { MP4File, MP4Info, MP4Sample, -} from "@webav/mp4box.js"; -import { sleep } from "../av-utils"; +} from '@webav/mp4box.js'; +import { sleep } from '../av-utils'; /** * 将原始字节流转换成 MP4Sample 流 @@ -12,12 +12,12 @@ import { sleep } from "../av-utils"; export class SampleTransform { readable: ReadableStream< | { - chunkType: "ready"; + chunkType: 'ready'; data: { info: MP4Info; file: MP4File }; } | { - chunkType: "samples"; - data: { id: number; type: "video" | "audio"; samples: MP4Sample[] }; + chunkType: 'samples'; + data: { id: number; type: 'video' | 'audio'; samples: MP4Sample[] }; } >; @@ -35,19 +35,19 @@ export class SampleTransform { file.onReady = (info) => { const vTrackId = info.videoTracks[0]?.id; if (vTrackId != null) - file.setExtractionOptions(vTrackId, "video", { nbSamples: 100 }); + file.setExtractionOptions(vTrackId, 'video', { nbSamples: 100 }); const aTrackId = info.audioTracks[0]?.id; if (aTrackId != null) - file.setExtractionOptions(aTrackId, "audio", { nbSamples: 100 }); + file.setExtractionOptions(aTrackId, 'audio', { nbSamples: 100 }); - ctrl.enqueue({ chunkType: "ready", data: { info, file } }); + ctrl.enqueue({ chunkType: 'ready', data: { info, file } }); file.start(); }; file.onSamples = (id, type, samples) => { ctrl.enqueue({ - chunkType: "samples", + chunkType: 'samples', data: { id, type, samples }, }); outCtrlDesiredSize = ctrl.desiredSize ?? 0; diff --git a/packages/av-cliper/src/offscreen-sprite.ts b/packages/av-cliper/src/offscreen-sprite.ts index b2a4542b..0ce69014 100644 --- a/packages/av-cliper/src/offscreen-sprite.ts +++ b/packages/av-cliper/src/offscreen-sprite.ts @@ -1,6 +1,6 @@ -import { BaseSprite } from "./base-sprite"; -import { IClip } from "./clips"; -import { Log } from "./log"; +import { BaseSprite } from './base-sprite'; +import { IClip } from './clips'; +import { Log } from './log'; export class OffscreenSprite extends BaseSprite { #clip: IClip; @@ -37,7 +37,7 @@ export class OffscreenSprite extends BaseSprite { super.render(ctx); const { w, h } = this.rect; const { video, audio, state } = await this.#clip.tick(time); - if (state === "done") { + if (state === 'done') { return { audio: audio ?? [], done: true, diff --git a/packages/av-cliper/src/rect.ts b/packages/av-cliper/src/rect.ts index 47d70d71..03127ae3 100644 --- a/packages/av-cliper/src/rect.ts +++ b/packages/av-cliper/src/rect.ts @@ -4,17 +4,17 @@ interface IPoint { } export type TCtrlKey = - | "t" - | "b" - | "l" - | "r" - | "lt" - | "lb" - | "rt" - | "rb" - | "rotate"; + | 't' + | 'b' + | 'l' + | 'r' + | 'lt' + | 'lb' + | 'rt' + | 'rb' + | 'rotate'; -export const CTRL_KEYS = ["t", "b", "l", "r", "lt", "lb", "rt", "rb", "rotate"]; +export const CTRL_KEYS = ['t', 'b', 'l', 'r', 'lt', 'lb', 'rt', 'rb', 'rotate']; export interface IRectBaseProps { x: number; diff --git a/packages/av-cliper/src/vite-env.d.ts b/packages/av-cliper/src/vite-env.d.ts index 951bda97..e058f21f 100644 --- a/packages/av-cliper/src/vite-env.d.ts +++ b/packages/av-cliper/src/vite-env.d.ts @@ -1 +1 @@ -import "vite/client"; +import 'vite/client'; diff --git a/packages/av-cliper/src/worker-timer.ts b/packages/av-cliper/src/worker-timer.ts index d1baca35..6562e461 100644 --- a/packages/av-cliper/src/worker-timer.ts +++ b/packages/av-cliper/src/worker-timer.ts @@ -4,14 +4,14 @@ const setup = (): void => { let interval: number = 16.6; self.onmessage = (e) => { - if (e.data.event === "start") { + if (e.data.event === 'start') { self.clearInterval(timerId); timerId = self.setInterval(() => { self.postMessage({}); }, interval); } - if (e.data.event === "stop") { + if (e.data.event === 'stop') { self.clearInterval(timerId); } }; @@ -48,7 +48,7 @@ export const workerTimer = ( handlerMap.set(groupId, fns); if (handlerMap.size === 1 && fns.size === 1) { - worker.postMessage({ event: "start" }); + worker.postMessage({ event: 'start' }); } return () => { @@ -56,7 +56,7 @@ export const workerTimer = ( if (fns.size === 0) handlerMap.delete(groupId); if (handlerMap.size === 0) { runCount = 0; - worker.postMessage({ event: "stop" }); + worker.postMessage({ event: 'stop' }); } }; }; diff --git a/packages/av-cliper/vite.config.ts b/packages/av-cliper/vite.config.ts index bfa48469..a268bb40 100644 --- a/packages/av-cliper/vite.config.ts +++ b/packages/av-cliper/vite.config.ts @@ -1,20 +1,20 @@ -import { resolve } from "path"; -import { defineConfig } from "vitest/config"; +import { resolve } from 'path'; +import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - environment: "jsdom", + environment: 'jsdom', coverage: { - provider: "istanbul", // or 'c8' + provider: 'istanbul', // or 'c8' }, onConsoleLog(msg) { - if (msg.includes("log test")) return false; + if (msg.includes('log test')) return false; }, }, build: { sourcemap: true, lib: { - entry: resolve(__dirname, "src/index.ts"), - name: "av-cliper", + entry: resolve(__dirname, 'src/index.ts'), + name: 'av-cliper', }, }, }); diff --git a/packages/av-recorder/demo/record-usermedia.ts b/packages/av-recorder/demo/record-usermedia.ts index 928ad5f0..89fb3297 100644 --- a/packages/av-recorder/demo/record-usermedia.ts +++ b/packages/av-recorder/demo/record-usermedia.ts @@ -64,7 +64,7 @@ pauseEl?.addEventListener('click', () => { })().catch(console.error); }); const continueEl = document.querySelector( - '#continueRecod' + '#continueRecod', ) as HTMLButtonElement; continueEl?.addEventListener('click', () => { (async () => { @@ -78,7 +78,7 @@ continueEl?.addEventListener('click', () => { }); async function createFileWriter( - extName: string + extName: string, ): Promise { const fileHandle = await window.showSaveFilePicker({ suggestedName: `WebAV-export-${Date.now()}.${extName}`, diff --git a/packages/av-recorder/src/av-exporter.ts b/packages/av-recorder/src/av-exporter.ts index 98a7abaa..8071c614 100644 --- a/packages/av-recorder/src/av-exporter.ts +++ b/packages/av-recorder/src/av-exporter.ts @@ -1,4 +1,4 @@ -import fixWebmDur from "fix-webm-duration"; +import fixWebmDur from 'fix-webm-duration'; /** * 导出 WebM 格式的视频, @@ -15,7 +15,7 @@ export async function exportWebM( ): Promise<() => void> { const recoder = new MediaRecorder(inputMediaStream, { ...recordOpts, - mimeType: "video/webm;codecs=avc1.64001f,opus", + mimeType: 'video/webm;codecs=avc1.64001f,opus', }); let firstBlob: Blob | null = null; recoder.ondataavailable = async (evt) => { diff --git a/packages/av-recorder/src/av-recorder.ts b/packages/av-recorder/src/av-recorder.ts index f1dc5b4e..a1002746 100644 --- a/packages/av-recorder/src/av-recorder.ts +++ b/packages/av-recorder/src/av-recorder.ts @@ -1,15 +1,15 @@ -import { Log } from "@webav/av-cliper"; -import MuxMP4Worker from "./mux-mp4-worker?worker&inline"; -import { EWorkerMsg, IRecorderConf, IStream, IWorkerOpts } from "./types"; +import { Log } from '@webav/av-cliper'; +import MuxMP4Worker from './mux-mp4-worker?worker&inline'; +import { EWorkerMsg, IRecorderConf, IStream, IWorkerOpts } from './types'; -type TState = "inactive" | "recording" | "paused"; +type TState = 'inactive' | 'recording' | 'paused'; export class AVRecorder { - #state: TState = "inactive"; - get state(): "inactive" | "recording" | "paused" { + #state: TState = 'inactive'; + get state(): 'inactive' | 'recording' | 'paused' { return this.#state; } set state(_: TState) { - throw new Error("state is readonly"); + throw new Error('state is readonly'); } #ms; @@ -27,14 +27,14 @@ export class AVRecorder { height: 720, bitrate: 3_000_000, expectFPS: 30, - audioCodec: "aac", - videoCodec: "avc1.42E032", + audioCodec: 'aac', + videoCodec: 'avc1.42E032', ...conf, }; } async start(timeSlice: number = 500): Promise { - Log.info("AVRecorder.start recoding"); + Log.info('AVRecorder.start recoding'); const worker = new MuxMP4Worker(); this.#worker = worker; @@ -47,7 +47,7 @@ export class AVRecorder { } const audioTrack = this.#ms.getAudioTracks()[0]; - let audioConf: IWorkerOpts["audio"] | null = null; + let audioConf: IWorkerOpts['audio'] | null = null; if (audioTrack != null) { const setting = audioTrack.getSettings(); audioConf = { @@ -55,14 +55,14 @@ export class AVRecorder { sampleRate: setting.sampleRate ?? 0, channelCount: setting.channelCount ?? 0, }; - Log.info("AVRecorder recording audioConf:", audioConf); + Log.info('AVRecorder recording audioConf:', audioConf); streams.audio = new MediaStreamTrackProcessor({ track: audioTrack, }).readable; } if (streams.audio == null && streams.video == null) { - throw new Error("No available tracks in MediaStream"); + throw new Error('No available tracks in MediaStream'); } const workerOpts: IWorkerOpts = { @@ -87,11 +87,11 @@ export class AVRecorder { ); return await new Promise((resolve) => { - worker.addEventListener("message", (evt: MessageEvent) => { + worker.addEventListener('message', (evt: MessageEvent) => { const { type, data } = evt.data; switch (type) { case EWorkerMsg.OutputStream: - this.#state = "recording"; + this.#state = 'recording'; this.outputStream = data; resolve(); break; @@ -108,13 +108,13 @@ export class AVRecorder { } async stop(): Promise { - this.#state = "inactive"; + this.#state = 'inactive'; const worker = this.#worker; if (worker == null) return; worker.postMessage({ type: EWorkerMsg.Stop }); return await new Promise((resolve) => { - worker.addEventListener("message", (evt: MessageEvent) => { + worker.addEventListener('message', (evt: MessageEvent) => { const { type } = evt.data; switch (type) { case EWorkerMsg.SafeExit: diff --git a/packages/av-recorder/src/mux-mp4-worker.ts b/packages/av-recorder/src/mux-mp4-worker.ts index e9e07e01..7c3a9651 100644 --- a/packages/av-recorder/src/mux-mp4-worker.ts +++ b/packages/av-recorder/src/mux-mp4-worker.ts @@ -1,15 +1,15 @@ -import mp4box from "@webav/mp4box.js"; -import { autoReadStream, file2stream, recodemux } from "@webav/av-cliper"; -import { TClearFn, EWorkerMsg, IWorkerOpts } from "./types"; +import mp4box from '@webav/mp4box.js'; +import { autoReadStream, file2stream, recodemux } from '@webav/av-cliper'; +import { TClearFn, EWorkerMsg, IWorkerOpts } from './types'; if (import.meta.env.DEV) { mp4box.Log.setLogLevel(mp4box.Log.debug); } enum State { - Preparing = "preparing", - Running = "running", - Stopped = "stopped", + Preparing = 'preparing', + Running = 'running', + Stopped = 'stopped', } class RecoderPauseCtrl { diff --git a/packages/av-recorder/src/types.ts b/packages/av-recorder/src/types.ts index 1f56bf96..7204db65 100644 --- a/packages/av-recorder/src/types.ts +++ b/packages/av-recorder/src/types.ts @@ -2,7 +2,7 @@ export interface IRecorderConf { width?: number; height?: number; expectFPS?: number; - audioCodec?: "aac"; + audioCodec?: 'aac'; videoCodec?: string; /* 码率 */ bitrate?: number; @@ -16,7 +16,7 @@ export interface IWorkerOpts { codec: string; }; audio: { - codec: "opus" | "aac"; + codec: 'opus' | 'aac'; sampleRate: number; channelCount: number; } | null; @@ -31,11 +31,11 @@ export interface IStream { } export enum EWorkerMsg { - SafeExit = "SafeExit", - Stop = "Stop", - Paused = "Paused", - Start = "Start", - OutputStream = "OutputStream", + SafeExit = 'SafeExit', + Stop = 'Stop', + Paused = 'Paused', + Start = 'Start', + OutputStream = 'OutputStream', } export type TClearFn = () => void; diff --git a/packages/av-recorder/src/vite-env.d.ts b/packages/av-recorder/src/vite-env.d.ts index 3809cb96..34f02176 100644 --- a/packages/av-recorder/src/vite-env.d.ts +++ b/packages/av-recorder/src/vite-env.d.ts @@ -1,2 +1,2 @@ -import "vite/client"; -import "@types/wicg-file-system-access"; +import 'vite/client'; +import '@types/wicg-file-system-access'; diff --git a/packages/av-recorder/vite.config.ts b/packages/av-recorder/vite.config.ts index 4434c4d4..3b5e2bfc 100644 --- a/packages/av-recorder/vite.config.ts +++ b/packages/av-recorder/vite.config.ts @@ -1,18 +1,18 @@ -import { defineConfig } from "vitest/config"; -import { resolve } from "path"; +import { defineConfig } from 'vitest/config'; +import { resolve } from 'path'; export default defineConfig({ test: { - environment: "jsdom", + environment: 'jsdom', coverage: { - provider: "istanbul", // or 'c8' + provider: 'istanbul', // or 'c8' }, }, build: { sourcemap: true, lib: { - entry: resolve(__dirname, "src/av-recorder.ts"), - name: "av-recoder", + entry: resolve(__dirname, 'src/av-recorder.ts'), + name: 'av-recoder', }, }, }); diff --git a/types/mp4box.d.ts b/types/mp4box.d.ts index dad33566..aef27513 100644 --- a/types/mp4box.d.ts +++ b/types/mp4box.d.ts @@ -1,4 +1,4 @@ -declare module "@webav/mp4box.js" { +declare module '@webav/mp4box.js' { export interface MP4MediaTrack { id: number; created: Date; @@ -122,7 +122,7 @@ declare module "@webav/mp4box.js" { } export interface TrakBoxParser extends BoxParser { - type: "trak"; + type: 'trak'; samples: MP4Sample[]; nextSample: number; sample_size: number; @@ -131,26 +131,26 @@ declare module "@webav/mp4box.js" { } interface MDATBoxParser extends BoxParser { - type: "mdat"; + type: 'mdat'; data: Uint8Array; } interface MOOFBoxParser extends BoxParser { - type: "moof"; + type: 'moof'; } interface MDIABoxParser extends BoxParser { - type: "mdia"; + type: 'mdia'; minf: MINFBoxParser; } interface MINFBoxParser extends BoxParser { - type: "minf"; + type: 'minf'; stbl: STBLBoxParser; } interface STBLBoxParser extends BoxParser { - type: "stbl"; + type: 'stbl'; stsd: STSDBoxParser; } @@ -159,7 +159,7 @@ declare module "@webav/mp4box.js" { } interface ESDSBoxParser extends BoxParser { - type: "esds"; + type: 'esds'; version: number; flags: number; esd: ESDBoxParser; @@ -167,27 +167,27 @@ declare module "@webav/mp4box.js" { } interface MOOVBoxParser extends BoxParser { - type: "moov"; + type: 'moov'; traks: TrakBoxParser[]; mvhd: MVHDBoxParser; } interface MVHDBoxParser extends BoxParser { - type: "mvhd"; + type: 'mvhd'; duration: number; timescale: number; } type STSDBoxParser = Omit< BoxParser & { - type: "stsd"; + type: 'stsd'; entries: Array; }, - "boxes" + 'boxes' >; export interface AVC1BoxParser extends BoxParser { - type: "avc1"; + type: 'avc1'; boxes: AVCCBox[]; avcC: AVCCBox; compressorname: string; @@ -199,7 +199,7 @@ declare module "@webav/mp4box.js" { } export interface HVCBoxParser extends BoxParser { - type: "hvc"; + type: 'hvc'; boxes: HVCCBox[]; hvcC: HVCCBox; compressorname: string; @@ -213,17 +213,17 @@ declare module "@webav/mp4box.js" { interface AVCCBox extends BoxParser { PPS: Array<{ length: number; nalu: Uint8Array }>; SPS: Array<{ length: number; nalu: Uint8Array }>; - type: "avcC"; + type: 'avcC'; } interface HVCCBox extends BoxParser { PPS: Array<{ length: number; nalu: Uint8Array }>; SPS: Array<{ length: number; nalu: Uint8Array }>; - type: "hvcC"; + type: 'hvcC'; } export interface MP4ABoxParser extends BoxParser { - type: "mp4a"; + type: 'mp4a'; channel_count: number; samplerate: number; samplesize: number;