| // META: title=VideoTrackGenerator tests. | |
| importScripts("/resources/testharness.js"); | |
| function make_audio_data(timestamp, channels, sampleRate, frames) { | |
| let data = new Float32Array(frames*channels); | |
| // This generates samples in a planar format. | |
| for (var channel = 0; channel < channels; channel++) { | |
| let hz = 100 + channel * 50; // sound frequency | |
| let base_index = channel * frames; | |
| for (var i = 0; i < frames; i++) { | |
| let t = (i / sampleRate) * hz * (Math.PI * 2); | |
| data[base_index + i] = Math.sin(t); | |
| } | |
| } | |
| return new AudioData({ | |
| timestamp: timestamp, | |
| data: data, | |
| numberOfChannels: channels, | |
| numberOfFrames: frames, | |
| sampleRate: sampleRate, | |
| format: "f32-planar", | |
| }); | |
| } | |
| const pixelColour = [50, 100, 150, 255]; | |
| const height = 240; | |
| const width = 320; | |
| function makeVideoFrame(timestamp) { | |
| const canvas = new OffscreenCanvas(width, height); | |
| const ctx = canvas.getContext('2d', {alpha: false}); | |
| ctx.fillStyle = `rgba(${pixelColour.join()})`; | |
| ctx.fillRect(0, 0, width, height); | |
| return new VideoFrame(canvas, {timestamp, alpha: 'discard'}); | |
| } | |
| promise_test(async t => { | |
| const videoFrame = makeVideoFrame(1); | |
| const originalWidth = videoFrame.displayWidth; | |
| const originalHeight = videoFrame.displayHeight; | |
| const originalTimestamp = videoFrame.timestamp; | |
| const generator = new VideoTrackGenerator(); | |
| t.add_cleanup(() => generator.track.stop()); | |
| // Use a MediaStreamTrackProcessor as a sink for |generator| to verify | |
| // that |processor| actually forwards the frames written to its writable | |
| // field. | |
| const processor = new MediaStreamTrackProcessor(generator); | |
| const reader = processor.readable.getReader(); | |
| const readerPromise = new Promise(async resolve => { | |
| const result = await reader.read(); | |
| t.add_cleanup(() => result.value.close()); | |
| t.step_func(() => { | |
| assert_equals(result.value.displayWidth, originalWidth); | |
| assert_equals(result.value.displayHeight, originalHeight); | |
| assert_equals(result.value.timestamp, originalTimestamp); | |
| })(); | |
| resolve(); | |
| }); | |
| generator.writable.getWriter().write(videoFrame); | |
| return readerPromise; | |
| }, 'Tests that VideoTrackGenerator forwards frames to sink'); | |
| promise_test(async t => { | |
| const generator = new VideoTrackGenerator(); | |
| t.add_cleanup(() => generator.track.stop()); | |
| const writer = generator.writable.getWriter(); | |
| const frame = makeVideoFrame(1); | |
| await writer.write(frame); | |
| assert_equals(generator.track.kind, "video"); | |
| assert_equals(generator.track.readyState, "live"); | |
| }, "Tests that creating a VideoTrackGenerator works as expected"); | |
| promise_test(async t => { | |
| const generator = new VideoTrackGenerator(); | |
| t.add_cleanup(() => generator.track.stop()); | |
| const writer = generator.writable.getWriter(); | |
| const frame = makeVideoFrame(1); | |
| await writer.write(frame); | |
| assert_throws_dom("InvalidStateError", () => frame.clone(), "VideoFrame wasn't destroyed on write."); | |
| }, "Tests that VideoFrames are destroyed on write"); | |
| promise_test(async t => { | |
| const generator = new VideoTrackGenerator(); | |
| t.add_cleanup(() => generator.track.stop()); | |
| const writer = generator.writable.getWriter(); | |
| if (!self.AudioData) | |
| return; | |
| const defaultInit = { | |
| timestamp: 1234, | |
| channels: 2, | |
| sampleRate: 8000, | |
| frames: 100, | |
| }; | |
| const audioData = make_audio_data(defaultInit.timestamp, defaultInit.channels, defaultInit.sampleRate, | |
| defaultInit.frames); | |
| await promise_rejects_js(t, TypeError, writer.write("test")); | |
| }, "Generator writer rejects on mismatched media input"); | |
| promise_test(async t => { | |
| const generator = new VideoTrackGenerator(); | |
| t.add_cleanup(() => generator.track.stop()); | |
| const writer = generator.writable.getWriter(); | |
| await promise_rejects_js(t, TypeError, writer.write("potatoe")); | |
| }, "Generator writer rejects on non media input"); | |
| promise_test(async t => { | |
| const generator = new VideoTrackGenerator(); | |
| const writer = generator.writable.getWriter(); | |
| const frame1 = makeVideoFrame(1); | |
| t.add_cleanup(() => frame1.close()); | |
| await writer.write(frame1); | |
| assert_equals(frame1.codedWidth, 0); | |
| generator.track.stop(); | |
| await writer.closed; | |
| const frame2 = makeVideoFrame(1); | |
| t.add_cleanup(() => frame2.close()); | |
| await promise_rejects_js(t, TypeError, writer.write(frame2)); | |
| assert_equals(frame2.codedWidth, 320); | |
| }, "A writer rejects when generator's track is stopped"); | |
| promise_test(async t => { | |
| const generator = new VideoTrackGenerator(); | |
| generator.muted = true; | |
| const writer = generator.writable.getWriter(); | |
| const frame1 = makeVideoFrame(1); | |
| t.add_cleanup(() => frame1.close()); | |
| await writer.write(frame1); | |
| assert_equals(frame1.codedWidth, 0); | |
| generator.track.stop(); | |
| await writer.closed; | |
| const frame2 = makeVideoFrame(1); | |
| t.add_cleanup(() => frame2.close()); | |
| await promise_rejects_js(t, TypeError, writer.write(frame2)); | |
| assert_equals(frame2.codedWidth, 320); | |
| }, "A muted writer rejects when generator's track is stopped"); | |
| promise_test(async t => { | |
| const generator = new VideoTrackGenerator(); | |
| const writer = generator.writable.getWriter(); | |
| const frame1 = makeVideoFrame(1); | |
| t.add_cleanup(() => frame1.close()); | |
| await writer.write(frame1); | |
| assert_equals(frame1.codedWidth, 0); | |
| const clonedTrack = generator.track.clone(); | |
| generator.track.stop(); | |
| await new Promise(resolve => t.step_timeout(resolve, 100)); | |
| const frame2 = makeVideoFrame(1); | |
| t.add_cleanup(() => frame2.close()); | |
| await writer.write(frame2); | |
| assert_equals(frame2.codedWidth, 0); | |
| clonedTrack.stop(); | |
| await writer.closed; | |
| const frame3 = makeVideoFrame(1); | |
| t.add_cleanup(() => frame3.close()); | |
| await promise_rejects_js(t, TypeError, writer.write(frame3)); | |
| assert_equals(frame3.codedWidth, 320); | |
| }, "A writer rejects when generator's track and clones are stopped"); | |
| promise_test(async t => { | |
| const generator = new VideoTrackGenerator(); | |
| t.add_cleanup(() => generator.track.stop()); | |
| // Use a MediaStreamTrackProcessor as a sink for |generator| to verify | |
| // that |processor| actually forwards the frames written to its writable | |
| // field. | |
| const processor = new MediaStreamTrackProcessor(generator); | |
| const reader = processor.readable.getReader(); | |
| const videoFrame = makeVideoFrame(1); | |
| const writer = generator.writable.getWriter(); | |
| const videoFrame1 = makeVideoFrame(1); | |
| writer.write(videoFrame1); | |
| const result1 = await reader.read(); | |
| t.add_cleanup(() => result1.value.close()); | |
| assert_equals(result1.value.timestamp, 1); | |
| generator.muted = true; | |
| // This frame is expected to be discarded. | |
| const videoFrame2 = makeVideoFrame(2); | |
| writer.write(videoFrame2); | |
| generator.muted = false; | |
| const videoFrame3 = makeVideoFrame(3); | |
| writer.write(videoFrame3); | |
| const result3 = await reader.read(); | |
| t.add_cleanup(() => result3.value.close()); | |
| assert_equals(result3.value.timestamp, 3); | |
| // Set up a read ahead of time, then mute, enqueue and unmute. | |
| const promise5 = reader.read(); | |
| generator.muted = true; | |
| writer.write(makeVideoFrame(4)); // Expected to be discarded. | |
| generator.muted = false; | |
| writer.write(makeVideoFrame(5)); | |
| const result5 = await promise5; | |
| t.add_cleanup(() => result5.value.close()); | |
| assert_equals(result5.value.timestamp, 5); | |
| }, 'Tests that VideoTrackGenerator forwards frames only when unmuted'); | |
| done(); |