// 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();