@@ -2,7 +2,7 @@ import { Fixture, SkipTestCase } from '../../common/framework/fixture.js';
22import { getResourcePath } from '../../common/framework/resources.js' ;
33import { keysOf } from '../../common/util/data_tables.js' ;
44import { timeout } from '../../common/util/timeout.js' ;
5- import { ErrorWithExtra , raceWithRejectOnTimeout } from '../../common/util/util.js' ;
5+ import { ErrorWithExtra , assert , raceWithRejectOnTimeout } from '../../common/util/util.js' ;
66import { GPUTest } from '../gpu_test.js' ;
77import { RGBA , srgbToDisplayP3 } from '../util/color_space_conversion.js' ;
88
@@ -559,59 +559,145 @@ function callbackHelper(
559559}
560560
561561/**
562- * Create VideoFrame from camera captured frame. Check whether browser environment has
563- * camera supported.
564- * Returns a webcodec VideoFrame.
565- *
566- * @param test: GPUTest that requires getting VideoFrame
567- *
562+ * Get a MediaStream from the default webcam via `getUserMedia()`.
568563 */
569- export async function captureCameraFrame ( test : GPUTest ) : Promise < VideoFrame > {
564+ async function getStreamFromCamera (
565+ test : Fixture ,
566+ videoTrackConstraints : MediaTrackConstraints | true
567+ ) : Promise < MediaStream > {
570568 test . skipIf ( typeof navigator === 'undefined' , 'navigator does not exist in this environment' ) ;
571569 test . skipIf (
572570 typeof navigator . mediaDevices === 'undefined' ||
573571 typeof navigator . mediaDevices . getUserMedia === 'undefined' ,
574572 "Browser doesn't support capture frame from camera."
575573 ) ;
576574
577- const stream = await navigator . mediaDevices . getUserMedia ( { video : true } ) ;
578- const track = stream . getVideoTracks ( ) [ 0 ] as MediaStreamVideoTrack ;
579-
580- test . skipIf ( ! track , "Doesn't have valid camera captured stream for testing." ) ;
575+ const stream = await navigator . mediaDevices . getUserMedia ( {
576+ audio : false ,
577+ video : videoTrackConstraints ,
578+ } ) ;
579+ test . trackForCleanup ( {
580+ close ( ) {
581+ for ( const track of stream . getTracks ( ) ) {
582+ track . stop ( ) ;
583+ }
584+ } ,
585+ } ) ;
586+ return stream ;
587+ }
581588
582- // Use MediaStreamTrackProcessor and ReadableStream to generate video frame directly.
583- if ( typeof MediaStreamTrackProcessor !== 'undefined' ) {
584- const trackProcessor = new MediaStreamTrackProcessor ( { track } ) ;
585- const reader = trackProcessor . readable . getReader ( ) ;
586- const result = await reader . read ( ) ;
587- if ( result . done ) {
588- test . skip ( 'MediaStreamTrackProcessor: Cannot get valid frame from readable stream.' ) ;
589+ /**
590+ * Chrome on macOS (at least) takes a while before it switches from blank frames
591+ * to real frames. Wait up to 50 frames for something to show up on the camera.
592+ */
593+ async function waitForNonBlankFrame ( {
594+ getSource,
595+ waitForNextFrame,
596+ } : {
597+ getSource : ( ) => HTMLVideoElement | VideoFrame ;
598+ waitForNextFrame : ( ) => Promise < void > ;
599+ } ) {
600+ const cvs = document . createElement ( 'canvas' ) ;
601+ [ cvs . width , cvs . height ] = [ 4 , 4 ] ;
602+ const ctx = cvs . getContext ( '2d' , { willReadFrequently : true } ) ! ;
603+ let foundNonBlankFrame = false ;
604+ for ( let i = 0 ; i < 50 ; ++ i ) {
605+ ctx . drawImage ( getSource ( ) , 0 , 0 , cvs . width , cvs . height ) ;
606+ const pixels = new Uint32Array ( ctx . getImageData ( 0 , 0 , cvs . width , cvs . height ) . data . buffer ) ;
607+ // Look only at RGB, ignore alpha.
608+ if ( pixels . some ( p => ( p & 0x00ffffff ) !== 0 ) ) {
609+ foundNonBlankFrame = true ;
610+ break ;
589611 }
590-
591- return result . value ;
592- }
593-
594- // Fallback to ImageCapture if MediaStreamTrackProcessor not supported. Using grabFrame() to
595- // generate imageBitmap and creating video frame from it.
596- if ( typeof ImageCapture !== 'undefined' ) {
597- const imageCapture = new ImageCapture ( track ) ;
598- const imageBitmap = await imageCapture . grabFrame ( ) ;
599- return new VideoFrame ( imageBitmap ) ;
612+ await waitForNextFrame ( ) ;
600613 }
614+ assert ( foundNonBlankFrame , 'Failed to get a non-blank video frame' ) ;
615+ }
601616
602- // Fallback to using HTMLVideoElement to do capture.
617+ /**
618+ * Uses MediaStreamTrackProcessor to capture a VideoFrame from the camera.
619+ * Skips the test if not supported.
620+ * @param videoTrackConstraints - MediaTrackConstraints (e.g. width/height) to pass to
621+ * `getUserMedia()`, or `true` if none.
622+ */
623+ export async function getVideoFrameFromCamera (
624+ test : Fixture ,
625+ videoTrackConstraints : MediaTrackConstraints | true
626+ ) : Promise < VideoFrame > {
603627 test . skipIf (
604- typeof HTMLVideoElement === 'undefined' ,
605- 'Try to use HTMLVideoElement do capture but HTMLVideoElement not available. '
628+ typeof MediaStreamTrackProcessor === 'undefined' ,
629+ 'MediaStreamTrackProcessor not supported '
606630 ) ;
607631
632+ const stream = await getStreamFromCamera ( test , videoTrackConstraints ) ;
633+ const tracks = stream . getVideoTracks ( ) ;
634+ assert ( tracks . length > 0 , 'no tracks found' ) ;
635+ const track = tracks [ 0 ] as MediaStreamVideoTrack ;
636+
637+ const trackProcessor = new MediaStreamTrackProcessor ( { track } ) ;
638+ const reader = trackProcessor . readable . getReader ( ) ;
639+
640+ const waitForNextFrame = async ( ) => {
641+ const result = await reader . read ( ) ;
642+ assert ( ! result . done , "MediaStreamTrackProcessor: Couldn't get valid frame from stream." ) ;
643+ return result . value ;
644+ } ;
645+ let frame : VideoFrame = await waitForNextFrame ( ) ;
646+ await waitForNonBlankFrame ( {
647+ getSource : ( ) => frame ,
648+ async waitForNextFrame ( ) {
649+ frame . close ( ) ;
650+ frame = await waitForNextFrame ( ) ;
651+ } ,
652+ } ) ;
653+
654+ test . trackForCleanup ( frame ) ;
655+ return frame ;
656+ }
657+
658+ /**
659+ * Create an HTMLVideoElement from the camera stream. Skips the test if not supported.
660+ * @param videoTrackConstraints - MediaTrackConstraints (e.g. width/height) to pass to
661+ * `getUserMedia()`, or `true` if none.
662+ * @param paused - whether the video should be paused before returning.
663+ */
664+ export async function getVideoElementFromCamera (
665+ test : Fixture ,
666+ videoTrackConstraints : MediaTrackConstraints | true ,
667+ paused : boolean
668+ ) : Promise < HTMLVideoElement > {
669+ const stream = await getStreamFromCamera ( test , videoTrackConstraints ) ;
670+
671+ // Main thread
608672 const video = document . createElement ( 'video' ) ;
673+ video . loop = false ;
674+ video . muted = true ;
675+ video . setAttribute ( 'playsinline' , '' ) ;
609676 video . srcObject = stream ;
677+ await new Promise ( resolve => {
678+ video . onloadedmetadata = resolve ;
679+ } ) ;
680+ await startPlayingAndWaitForVideo ( video , ( ) => { } ) ;
610681
611- const frame = await getVideoFrameFromVideoElement ( test , video ) ;
612- test . trackForCleanup ( frame ) ;
682+ await waitForNonBlankFrame ( {
683+ getSource : ( ) => video ,
684+ waitForNextFrame : ( ) =>
685+ new Promise ( resolve =>
686+ video . requestVideoFrameCallback ( ( ) => {
687+ resolve ( ) ;
688+ } )
689+ ) ,
690+ } ) ;
613691
614- return frame ;
692+ if ( paused ) {
693+ // Pause the video so we get consistent readbacks.
694+ await new Promise ( resolve => {
695+ video . onpause = resolve ;
696+ video . pause ( ) ;
697+ } ) ;
698+ }
699+
700+ return video ;
615701}
616702
617703const kFourColorsInfo = {
0 commit comments