|
5 | 5 | * Public License, v. 2.0 that can be found in the LICENSE file. |
6 | 6 | */ |
7 | 7 |
|
| 8 | +import * as PIXI from 'pixi.js'; |
8 | 9 | import { describe, expect, it, beforeEach, vi, afterEach, afterAll, MockInstance } from 'vitest'; |
9 | 10 | import { Composition } from './composition'; |
10 | | -import { Clip, TextClip } from '../clips'; |
| 11 | +import { AudioClip, Clip, TextClip } from '../clips'; |
11 | 12 | import { AudioTrack, CaptionTrack, HtmlTrack, ImageTrack, TextTrack, Track, VideoTrack } from '../tracks'; |
12 | 13 | import { Timestamp } from '../models'; |
| 14 | +import { sleep } from '../utils'; |
| 15 | +import { AudioBufferMock, OfflineAudioContextMock } from '../../vitest.mocks'; |
| 16 | +import { AudioSource } from '../sources'; |
13 | 17 |
|
14 | 18 | describe('The composition', () => { |
15 | 19 | let composition: Composition; |
@@ -49,6 +53,17 @@ describe('The composition', () => { |
49 | 53 | expect(composition.playing).toBe(false); |
50 | 54 | }); |
51 | 55 |
|
| 56 | + it("should trigger an error when the composition can't be initialized", async () => { |
| 57 | + const errorFn = vi.fn(); |
| 58 | + vi.spyOn(PIXI, 'autoDetectRenderer') |
| 59 | + .mockImplementationOnce(() => Promise.reject(new Error('Mocked rejection'))); |
| 60 | + const composition = new Composition(); |
| 61 | + composition.on('error', errorFn); |
| 62 | + expect(errorFn).toBeCalledTimes(0); |
| 63 | + await sleep(1); |
| 64 | + expect(errorFn).toBeCalledTimes(1); |
| 65 | + }) |
| 66 | + |
52 | 67 | it('should return width and height', () => { |
53 | 68 | expect(composition.settings.height).toBe(composition.height); |
54 | 69 | expect(composition.settings.width).toBe(composition.width); |
@@ -83,11 +98,14 @@ describe('The composition', () => { |
83 | 98 | expect(composition.duration.frames).toBe(8 * 30); |
84 | 99 | }); |
85 | 100 |
|
86 | | - it('should append canvas to div', () => { |
| 101 | + it('should attach a canvas to the dom and be able to remove it', () => { |
87 | 102 | const div = document.createElement('div'); |
88 | 103 | composition.attachPlayer(div); |
89 | 104 | expect(div.children.length).toBe(1); |
90 | 105 | expect(div.children[0] instanceof HTMLCanvasElement).toBe(true); |
| 106 | + |
| 107 | + composition.detachPlayer(div); |
| 108 | + expect(div.children.length).toBe(0); |
91 | 109 | }); |
92 | 110 |
|
93 | 111 | it('should append new tracks', () => { |
@@ -167,6 +185,22 @@ describe('The composition', () => { |
167 | 185 | expect(search4.length).toBe(0); |
168 | 186 | }); |
169 | 187 |
|
| 188 | + it('should seek a time by timestamp', async () => { |
| 189 | + const clip = new Clip({ stop: 15 }); |
| 190 | + |
| 191 | + const track = composition.createTrack('base'); |
| 192 | + await track.add(clip); |
| 193 | + |
| 194 | + const seekMock = vi.spyOn(track, 'seek'); |
| 195 | + computeMock.mockClear(); |
| 196 | + |
| 197 | + const ts = new Timestamp(400) // 12 frames |
| 198 | + composition.seek(ts); |
| 199 | + expect(composition.frame).toBe(12); |
| 200 | + expect(seekMock).toBeCalledTimes(1); |
| 201 | + expect(seekMock.mock.calls[0][0].millis).toBe(400); |
| 202 | + }); |
| 203 | + |
170 | 204 | it('should render clips when user called play', async () => { |
171 | 205 | const clip = new Clip({ stop: 15 }); |
172 | 206 |
|
@@ -239,16 +273,22 @@ describe('The composition', () => { |
239 | 273 | }); |
240 | 274 |
|
241 | 275 | it('should be able to screenshot a frame', async () => { |
242 | | - const clip = new Clip({ stop: 6 * 30 }); |
243 | | - const track = composition.createTrack('base'); |
244 | | - await track.add(clip); |
| 276 | + await composition.add(new Clip({ stop: 6 * 30 })); |
245 | 277 |
|
246 | 278 | composition.frame = 10; |
247 | 279 | expect(composition.screenshot()).toBe('data:image/png;base64,00'); |
248 | 280 | expect(composition.screenshot('webp')).toBe('data:image/webp;base64,00'); |
249 | 281 | expect(composition.screenshot('jpeg')).toBe('data:image/jpeg;base64,00'); |
250 | 282 | }); |
251 | 283 |
|
| 284 | + it('should not screenshot a frame when the renderer is undefined', async () => { |
| 285 | + await composition.add(new Clip({ stop: 6 * 30 })); |
| 286 | + |
| 287 | + delete composition.renderer; |
| 288 | + |
| 289 | + expect(() => composition.screenshot()).toThrowError(); |
| 290 | + }); |
| 291 | + |
252 | 292 | it('should be able to calculate the correct time', async () => { |
253 | 293 | composition.duration = Timestamp.fromFrames(20 * 30); |
254 | 294 | composition.frame = 10 * 30; |
@@ -425,3 +465,55 @@ describe('The composition', () => { |
425 | 465 | requestAnimationFrameMock.mockClear(); |
426 | 466 | }); |
427 | 467 | }); |
| 468 | + |
| 469 | +describe('Composition audio', () => { |
| 470 | + vi.stubGlobal('OfflineAudioContext', OfflineAudioContextMock); |
| 471 | + |
| 472 | + const source = new AudioSource(); |
| 473 | + |
| 474 | + vi.spyOn(source, 'decode').mockImplementation(async ( |
| 475 | + numberOfChannels: number = 2, |
| 476 | + sampleRate: number = 48000, |
| 477 | + ) => { |
| 478 | + const buffer = new AudioBufferMock({ numberOfChannels, sampleRate, length: 8000 }); |
| 479 | + |
| 480 | + for (let i = 0; i < buffer.channelData.length; i++) { |
| 481 | + for (let j = 0; j < buffer.channelData[i].length; j++) { |
| 482 | + buffer.channelData[i][j] = 1; |
| 483 | + } |
| 484 | + } |
| 485 | + |
| 486 | + return buffer as any; |
| 487 | + }); |
| 488 | + |
| 489 | + vi.spyOn(source, 'createObjectURL').mockImplementationOnce(async () => ''); |
| 490 | + |
| 491 | + const clip = new AudioClip(source); |
| 492 | + |
| 493 | + vi.spyOn(clip.element, 'oncanplay', 'set') |
| 494 | + .mockImplementation(function (this: HTMLMediaElement, fn) { |
| 495 | + fn?.call(this, new Event('canplay')); |
| 496 | + }); |
| 497 | + |
| 498 | + vi.spyOn(clip.element, 'duration', 'get').mockReturnValue(1); |
| 499 | + |
| 500 | + it('should merge audio clips', async () => { |
| 501 | + const composition = new Composition(); |
| 502 | + await composition.add(clip.subclip(2, 26)); |
| 503 | + |
| 504 | + expect(composition.duration.frames).toBe(26); |
| 505 | + composition.duration = 30; |
| 506 | + expect(composition.duration.frames).toBe(30); |
| 507 | + |
| 508 | + const buffer = await composition.audio(1, 8000); |
| 509 | + expect(buffer.length).toBe(8000); |
| 510 | + expect(buffer.numberOfChannels).toBe(1); |
| 511 | + expect(buffer.sampleRate).toBe(8000); |
| 512 | + |
| 513 | + const data = buffer.getChannelData(0); |
| 514 | + // audio clip has been trimmed and contains all ones |
| 515 | + expect(data.at(0)).toBe(0); |
| 516 | + expect(data.at(-1)).toBe(0); |
| 517 | + expect(data.at(4000)).toBe(1); |
| 518 | + }); |
| 519 | +}); |
0 commit comments